Skip to content

Commit ea0a031

Browse files
feat: button to publish a container [FC-0083] (#1827)
- Publish button with functionality of publish units and components inside the unit
1 parent ea8a8e5 commit ea0a031

7 files changed

Lines changed: 160 additions & 49 deletions

File tree

src/library-authoring/containers/UnitInfo.test.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ import {
88
import { mockContentLibrary, mockGetContainerMetadata } from '../data/api.mocks';
99
import { LibraryProvider } from '../common/context/LibraryContext';
1010
import UnitInfo from './UnitInfo';
11-
import { getLibraryContainerApiUrl } from '../data/api';
11+
import { getLibraryContainerApiUrl, getLibraryContainerPublishApiUrl } from '../data/api';
1212
import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext';
1313

1414
mockGetContainerMetadata.applyMock();
15+
mockContentLibrary.applyMock();
16+
mockGetContainerMetadata.applyMock();
17+
1518
const { libraryId } = mockContentLibrary;
1619
const { containerId } = mockGetContainerMetadata;
20+
1721
const render = () => baseRender(<UnitInfo />, {
1822
extraWrapper: ({ children }) => (
1923
<LibraryProvider
@@ -38,7 +42,7 @@ describe('<UnitInfo />', () => {
3842
({ axiosMock, mockShowToast } = initializeMocks());
3943
});
4044

41-
it('should detele the unit using the menu', async () => {
45+
it('should delete the unit using the menu', async () => {
4246
axiosMock.onDelete(getLibraryContainerApiUrl(containerId)).reply(200);
4347
render();
4448

@@ -61,4 +65,34 @@ describe('<UnitInfo />', () => {
6165
});
6266
expect(mockShowToast).toHaveBeenCalled();
6367
});
68+
69+
it('can publish the container', async () => {
70+
axiosMock.onPost(getLibraryContainerPublishApiUrl(containerId)).reply(200);
71+
render();
72+
73+
// Click on Publish button
74+
const publishButton = await screen.findByRole('button', { name: 'Publish' });
75+
expect(publishButton).toBeInTheDocument();
76+
userEvent.click(publishButton);
77+
78+
await waitFor(() => {
79+
expect(axiosMock.history.post.length).toBe(1);
80+
});
81+
expect(mockShowToast).toHaveBeenCalledWith('All changes published');
82+
});
83+
84+
it('shows an error if publishing the container fails', async () => {
85+
axiosMock.onPost(getLibraryContainerPublishApiUrl(containerId)).reply(500);
86+
render();
87+
88+
// Click on Publish button
89+
const publishButton = await screen.findByRole('button', { name: 'Publish' });
90+
expect(publishButton).toBeInTheDocument();
91+
userEvent.click(publishButton);
92+
93+
await waitFor(() => {
94+
expect(axiosMock.history.post.length).toBe(1);
95+
});
96+
expect(mockShowToast).toHaveBeenCalledWith('Failed to publish changes');
97+
});
6498
});

src/library-authoring/containers/UnitInfo.tsx

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
IconButton,
1010
useToggle,
1111
} from '@openedx/paragon';
12-
import { useEffect, useCallback } from 'react';
12+
import React, { useEffect, useCallback } from 'react';
1313
import { Link } from 'react-router-dom';
1414
import { MoreVert } from '@openedx/paragon/icons';
1515

@@ -28,7 +28,8 @@ import { LibraryUnitBlocks } from '../units/LibraryUnitBlocks';
2828
import messages from './messages';
2929
import componentMessages from '../components/messages';
3030
import ContainerDeleter from '../components/ContainerDeleter';
31-
import { useContainer } from '../data/apiHooks';
31+
import { useContainer, usePublishContainer } from '../data/apiHooks';
32+
import { ToastContext } from '../../generic/toast-context';
3233

3334
type ContainerMenuProps = {
3435
containerId: string,
@@ -71,8 +72,9 @@ const UnitMenu = ({ containerId, displayName }: ContainerMenuProps) => {
7172
const UnitInfo = () => {
7273
const intl = useIntl();
7374

74-
const { libraryId } = useLibraryContext();
75+
const { libraryId, readOnly } = useLibraryContext();
7576
const { componentPickerMode } = useComponentPickerContext();
77+
const { showToast } = React.useContext(ToastContext);
7678
const {
7779
defaultTab,
7880
hiddenTabs,
@@ -90,6 +92,7 @@ const UnitInfo = () => {
9092

9193
const unitId = sidebarComponentInfo?.id;
9294
const { data: container } = useContainer(unitId);
95+
const publishContainer = usePublishContainer(unitId!);
9396

9497
const showOpenUnitButton = !insideUnit && !componentPickerMode;
9598

@@ -105,6 +108,15 @@ const UnitInfo = () => {
105108
);
106109
}, [hiddenTabs, defaultTab.unit, unitId]);
107110

111+
const handlePublish = React.useCallback(async () => {
112+
try {
113+
await publishContainer.mutateAsync();
114+
showToast(intl.formatMessage(messages.publishContainerSuccess));
115+
} catch (error) {
116+
showToast(intl.formatMessage(messages.publishContainerFailed));
117+
}
118+
}, [publishContainer]);
119+
108120
useEffect(() => {
109121
// Show Organize tab if JumpToAddCollections action is set in sidebarComponentInfo
110122
if (jumpToCollections) {
@@ -118,8 +130,8 @@ const UnitInfo = () => {
118130

119131
return (
120132
<Stack>
121-
{showOpenUnitButton && (
122-
<div className="d-flex flex-wrap">
133+
<div className="d-flex flex-wrap">
134+
{showOpenUnitButton && (
123135
<Button
124136
variant="outline-primary"
125137
className="m-1 text-nowrap flex-grow-1"
@@ -128,12 +140,24 @@ const UnitInfo = () => {
128140
>
129141
{intl.formatMessage(messages.openUnitButton)}
130142
</Button>
143+
)}
144+
{!componentPickerMode && !readOnly && (
145+
<Button
146+
variant="outline-primary"
147+
className="m-1 text-nowrap flex-grow-1"
148+
disabled={!container.hasUnpublishedChanges || publishContainer.isLoading}
149+
onClick={handlePublish}
150+
>
151+
{intl.formatMessage(messages.publishContainerButton)}
152+
</Button>
153+
)}
154+
{showOpenUnitButton && ( // Check: should we still show this on the unit page?
131155
<UnitMenu
132156
containerId={unitId}
133157
displayName={container.displayName}
134158
/>
135-
</div>
136-
)}
159+
)}
160+
</div>
137161
<Tabs
138162
variant="tabs"
139163
className="my-3 d-flex justify-content-around"

src/library-authoring/containers/messages.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ const messages = defineMessages({
2626
defaultMessage: 'Collections ({count})',
2727
description: 'Title for collections section in organize tab',
2828
},
29+
publishContainerButton: {
30+
id: 'course-authoring.library-authoring.container-sidebar.publish-button',
31+
defaultMessage: 'Publish',
32+
description: 'Button text to publish the unit/subsection/section',
33+
},
34+
publishContainerSuccess: {
35+
id: 'course-authoring.library-authoring.container-sidebar.publish-success',
36+
defaultMessage: 'All changes published',
37+
description: 'Popup text after publishing a unit/subsection/section',
38+
},
39+
publishContainerFailed: {
40+
id: 'course-authoring.library-authoring.container-sidebar.publish-failure',
41+
defaultMessage: 'Failed to publish changes',
42+
description: 'Popup text seen if publishing a unit/subsection/section fails',
43+
},
2944
settingsTabTitle: {
3045
id: 'course-authoring.library-authoring.container-sidebar.settings-tab.title',
3146
defaultMessage: 'Settings',

src/library-authoring/data/api.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ export const getLibraryContainerChildrenApiUrl = (containerId: string) => `${get
124124
* Get the URL for library container collections.
125125
*/
126126
export const getLibraryContainerCollectionsUrl = (containerId: string) => `${getLibraryContainerApiUrl(containerId)}collections/`;
127+
/**
128+
* Get the URL for the API endpoint to publish a single container (+ children).
129+
*/
130+
export const getLibraryContainerPublishApiUrl = (containerId: string) => `${getLibraryContainerApiUrl(containerId)}publish/`;
127131

128132
export interface ContentLibrary {
129133
id: string;
@@ -700,3 +704,13 @@ export async function removeLibraryContainerChildren(
700704
);
701705
return camelCaseObject(data);
702706
}
707+
708+
/**
709+
* Publish a container, and any unpublished children within it.
710+
*
711+
* This doesn't return any data at the moment, but we could have it return a
712+
* list of the auto-published children in the future, if that would be helpful.
713+
*/
714+
export async function publishContainer(containerId: string) {
715+
await getAuthenticatedHttpClient().post(getLibraryContainerPublishApiUrl(containerId));
716+
}

src/library-authoring/data/apiHooks.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
getLibraryContainerApiUrl,
1616
getLibraryContainerRestoreApiUrl,
1717
getLibraryContainerChildrenApiUrl,
18+
getLibraryContainerPublishApiUrl,
1819
} from './api';
1920
import {
2021
useCommitLibraryChanges,
@@ -31,6 +32,7 @@ import {
3132
useAddComponentsToContainer,
3233
useUpdateContainerChildren,
3334
useRemoveContainerChildren,
35+
usePublishContainer,
3436
} from './apiHooks';
3537

3638
let axiosMock;
@@ -308,4 +310,16 @@ describe('library api hooks', () => {
308310
expect(axiosMock.history.patch.length).toEqual(0);
309311
});
310312
});
313+
314+
describe('publishContainer', () => {
315+
it('should publish a container', async () => {
316+
const containerId = 'lct:org:lib:unit:1';
317+
const url = getLibraryContainerPublishApiUrl(containerId);
318+
axiosMock.onPost(url).reply(200);
319+
const { result } = renderHook(() => usePublishContainer(containerId), { wrapper });
320+
await result.current.mutateAsync();
321+
322+
expect(axiosMock.history.post[0].url).toEqual(url);
323+
});
324+
});
311325
});

0 commit comments

Comments
 (0)