{
+ const client = getAuthenticatedHttpClient();
+ const { data } = await client.patch(getContentLibraryApiUrl(libraryData.id), libraryData);
+
+ return camelCaseObject(data);
+}
+
/**
* Get a list of content libraries.
*/
@@ -140,3 +169,19 @@ export async function getContentLibraryV2List(customParams: GetLibrariesV2Custom
.get(getContentLibraryV2ListApiUrl(), { params: customParamsFormated });
return camelCaseObject(data);
}
+
+/**
+ * Commit library changes.
+ */
+export async function commitLibraryChanges(libraryId: string) {
+ const client = getAuthenticatedHttpClient();
+ await client.post(getCommitLibraryChangesUrl(libraryId));
+}
+
+/**
+ * Revert library changes.
+ */
+export async function revertLibraryChanges(libraryId: string) {
+ const client = getAuthenticatedHttpClient();
+ await client.delete(getCommitLibraryChangesUrl(libraryId));
+}
diff --git a/src/library-authoring/data/apiHooks.test.tsx b/src/library-authoring/data/apiHooks.test.tsx
index 679842376..686e11401 100644
--- a/src/library-authoring/data/apiHooks.test.tsx
+++ b/src/library-authoring/data/apiHooks.test.tsx
@@ -5,8 +5,8 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { renderHook } from '@testing-library/react-hooks';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import MockAdapter from 'axios-mock-adapter';
-import { getCreateLibraryBlockUrl } from './api';
-import { useCreateLibraryBlock } from './apiHooks';
+import { getCommitLibraryChangesUrl, getCreateLibraryBlockUrl } from './api';
+import { useCommitLibraryChanges, useCreateLibraryBlock, useRevertLibraryChanges } from './apiHooks';
let axiosMock;
@@ -50,4 +50,24 @@ describe('library api hooks', () => {
expect(axiosMock.history.post[0].url).toEqual(url);
});
+
+ it('should commit library changes', async () => {
+ const libraryId = 'lib:org:1';
+ const url = getCommitLibraryChangesUrl(libraryId);
+ axiosMock.onPost(url).reply(200);
+ const { result } = renderHook(() => useCommitLibraryChanges(), { wrapper });
+ await result.current.mutateAsync(libraryId);
+
+ expect(axiosMock.history.post[0].url).toEqual(url);
+ });
+
+ it('should revert library changes', async () => {
+ const libraryId = 'lib:org:1';
+ const url = getCommitLibraryChangesUrl(libraryId);
+ axiosMock.onDelete(url).reply(200);
+ const { result } = renderHook(() => useRevertLibraryChanges(), { wrapper });
+ await result.current.mutateAsync(libraryId);
+
+ expect(axiosMock.history.delete[0].url).toEqual(url);
+ });
});
diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts
index 4685a6935..c09be62c9 100644
--- a/src/library-authoring/data/apiHooks.ts
+++ b/src/library-authoring/data/apiHooks.ts
@@ -6,6 +6,10 @@ import {
getLibraryBlockTypes,
createLibraryBlock,
getContentLibraryV2List,
+ commitLibraryChanges,
+ revertLibraryChanges,
+ updateLibraryMetadata,
+ ContentLibrary,
} from './api';
export const libraryAuthoringQueryKeys = {
@@ -61,6 +65,35 @@ export const useCreateLibraryBlock = () => {
});
};
+export const useUpdateLibraryMetadata = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: updateLibraryMetadata,
+ onMutate: async (data) => {
+ const queryKey = libraryAuthoringQueryKeys.contentLibrary(data.id);
+ const previousLibraryData = queryClient.getQueriesData(queryKey)[0][1] as ContentLibrary;
+
+ const newLibraryData = {
+ ...previousLibraryData,
+ title: data.title,
+ };
+
+ queryClient.setQueryData(queryKey, newLibraryData);
+
+ return { previousLibraryData, newLibraryData };
+ },
+ onError: (_err, data, context) => {
+ queryClient.setQueryData(
+ libraryAuthoringQueryKeys.contentLibrary(data.id),
+ context?.previousLibraryData,
+ );
+ },
+ onSettled: (_data, _error, variables) => {
+ queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(variables.id) });
+ },
+ });
+};
+
/**
* Builds the query to fetch list of V2 Libraries
*/
@@ -71,3 +104,23 @@ export const useContentLibraryV2List = (customParams: GetLibrariesV2CustomParams
keepPreviousData: true,
})
);
+
+export const useCommitLibraryChanges = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: commitLibraryChanges,
+ onSettled: (_data, _error, libraryId) => {
+ queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) });
+ },
+ });
+};
+
+export const useRevertLibraryChanges = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: revertLibraryChanges,
+ onSettled: (_data, _error, libraryId) => {
+ queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) });
+ },
+ });
+};
diff --git a/src/library-authoring/index.scss b/src/library-authoring/index.scss
index 87c22f838..e82ba16ab 100644
--- a/src/library-authoring/index.scss
+++ b/src/library-authoring/index.scss
@@ -1 +1,2 @@
@import "library-authoring/components/ComponentCard";
+@import "library-authoring/library-info/LibraryPublishStatus";
diff --git a/src/library-authoring/library-info/LibraryInfo.test.tsx b/src/library-authoring/library-info/LibraryInfo.test.tsx
new file mode 100644
index 000000000..5ace15eb9
--- /dev/null
+++ b/src/library-authoring/library-info/LibraryInfo.test.tsx
@@ -0,0 +1,207 @@
+import React from 'react';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { AppProvider } from '@edx/frontend-platform/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { initializeMockApp } from '@edx/frontend-platform';
+import MockAdapter from 'axios-mock-adapter';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import {
+ render,
+ screen,
+ fireEvent,
+ waitFor,
+} from '@testing-library/react';
+import LibraryInfo from './LibraryInfo';
+import { ToastProvider } from '../../generic/toast-context';
+import { ContentLibrary, getCommitLibraryChangesUrl } from '../data/api';
+import initializeStore from '../../store';
+
+let store;
+let axiosMock;
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+});
+
+const libraryData: ContentLibrary = {
+ id: 'lib:org1:lib1',
+ type: 'complex',
+ org: 'org1',
+ slug: 'lib1',
+ title: 'lib1',
+ description: 'lib1',
+ numBlocks: 2,
+ version: 0,
+ lastPublished: null,
+ lastDraftCreated: '2024-07-22',
+ publishedBy: 'staff',
+ lastDraftCreatedBy: 'staff',
+ allowLti: false,
+ allowPublicLearning: false,
+ allowPublicRead: false,
+ hasUnpublishedChanges: true,
+ hasUnpublishedDeletes: false,
+ canEditLibrary: true,
+ license: '',
+ created: '2024-06-26',
+ updated: '2024-07-20',
+};
+
+interface WrapperProps {
+ data: ContentLibrary,
+}
+
+const RootWrapper = ({ data } : WrapperProps) => (
+
+
+
+
+
+
+
+
+
+);
+
+describe('', () => {
+ beforeEach(() => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: true,
+ roles: [],
+ },
+ });
+ store = initializeStore();
+ axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ axiosMock.restore();
+ });
+
+ it('should render Library info sidebar', () => {
+ render();
+
+ expect(screen.getByText('Draft')).toBeInTheDocument();
+ expect(screen.getByText('(Never Published)')).toBeInTheDocument();
+ expect(screen.getByText('July 22, 2024')).toBeInTheDocument();
+ expect(screen.getByText('staff')).toBeInTheDocument();
+ expect(screen.getByText(libraryData.org)).toBeInTheDocument();
+ expect(screen.getByText('July 20, 2024')).toBeInTheDocument();
+ expect(screen.getByText('June 26, 2024')).toBeInTheDocument();
+ });
+
+ it('should render Library info in draft state without user', () => {
+ const data = {
+ ...libraryData,
+ lastDraftCreatedBy: null,
+ };
+
+ render();
+
+ expect(screen.getByText('Draft')).toBeInTheDocument();
+ expect(screen.getByText('(Never Published)')).toBeInTheDocument();
+ expect(screen.getByText('July 22, 2024')).toBeInTheDocument();
+ expect(screen.queryByText('staff')).not.toBeInTheDocument();
+ });
+
+ it('should render Library creation date if last draft created date is null', () => {
+ const data = {
+ ...libraryData,
+ lastDraftCreated: null,
+ };
+
+ render();
+
+ expect(screen.getByText('Draft')).toBeInTheDocument();
+ expect(screen.getByText('(Never Published)')).toBeInTheDocument();
+ expect(screen.getAllByText('June 26, 2024')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('June 26, 2024')[1]).toBeInTheDocument();
+ });
+
+ it('should render library info in draft state without date', () => {
+ const data = {
+ ...libraryData,
+ lastDraftCreated: null,
+ created: null,
+ };
+
+ render();
+
+ expect(screen.getByText('Draft')).toBeInTheDocument();
+ expect(screen.getByText('(Never Published)')).toBeInTheDocument();
+ });
+
+ it('should render draft library info sidebar', () => {
+ const data = {
+ ...libraryData,
+ lastPublished: '2024-07-26',
+ };
+
+ render();
+
+ expect(screen.getByText('Draft')).toBeInTheDocument();
+ expect(screen.queryByText('(Never Published)')).not.toBeInTheDocument();
+ expect(screen.getByText('July 22, 2024')).toBeInTheDocument();
+ expect(screen.getByText('staff')).toBeInTheDocument();
+ });
+
+ it('should render published library info sidebar', () => {
+ const data = {
+ ...libraryData,
+ lastPublished: '2024-07-26',
+ hasUnpublishedChanges: false,
+ };
+
+ render();
+ expect(screen.getByText('Published')).toBeInTheDocument();
+ expect(screen.getByText('July 26, 2024')).toBeInTheDocument();
+ expect(screen.getByText('staff')).toBeInTheDocument();
+ });
+
+ it('should render published library info without user', () => {
+ const data = {
+ ...libraryData,
+ lastPublished: '2024-07-26',
+ hasUnpublishedChanges: false,
+ publishedBy: null,
+ };
+
+ render();
+ expect(screen.getByText('Published')).toBeInTheDocument();
+ expect(screen.getByText('July 26, 2024')).toBeInTheDocument();
+ expect(screen.queryByText('staff')).not.toBeInTheDocument();
+ });
+
+ it('should publish library', async () => {
+ const url = getCommitLibraryChangesUrl(libraryData.id);
+ axiosMock.onPost(url).reply(200);
+ render();
+
+ const publishButton = screen.getByRole('button', { name: /publish/i });
+ fireEvent.click(publishButton);
+
+ expect(await screen.findByText('Library published successfully')).toBeInTheDocument();
+
+ await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(url));
+ });
+
+ it('should show error on publish library', async () => {
+ const url = getCommitLibraryChangesUrl(libraryData.id);
+ axiosMock.onPost(url).reply(500);
+ render();
+
+ const publishButton = screen.getByRole('button', { name: /publish/i });
+ fireEvent.click(publishButton);
+
+ expect(await screen.findByText('There was an error publishing the library.')).toBeInTheDocument();
+
+ await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(url));
+ });
+});
diff --git a/src/library-authoring/library-info/LibraryInfo.tsx b/src/library-authoring/library-info/LibraryInfo.tsx
new file mode 100644
index 000000000..b14b8d621
--- /dev/null
+++ b/src/library-authoring/library-info/LibraryInfo.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { Stack } from '@openedx/paragon';
+import { FormattedDate, useIntl } from '@edx/frontend-platform/i18n';
+import messages from './messages';
+import LibraryPublishStatus from './LibraryPublishStatus';
+import { ContentLibrary } from '../data/api';
+
+type LibraryInfoProps = {
+ library: ContentLibrary,
+};
+
+const LibraryInfo = ({ library } : LibraryInfoProps) => {
+ const intl = useIntl();
+
+ return (
+
+
+
+
+ {intl.formatMessage(messages.organizationSectionTitle)}
+
+
+ {library.org}
+
+
+
+
+ {intl.formatMessage(messages.libraryHistorySectionTitle)}
+
+
+
+ {intl.formatMessage(messages.lastModifiedLabel)}
+
+
+
+
+
+
+
+ {intl.formatMessage(messages.createdLabel)}
+
+
+
+
+
+
+
+ );
+};
+
+export default LibraryInfo;
diff --git a/src/library-authoring/library-info/LibraryInfoHeader.test.tsx b/src/library-authoring/library-info/LibraryInfoHeader.test.tsx
new file mode 100644
index 000000000..c67e9ed0d
--- /dev/null
+++ b/src/library-authoring/library-info/LibraryInfoHeader.test.tsx
@@ -0,0 +1,159 @@
+import React from 'react';
+import MockAdapter from 'axios-mock-adapter';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { AppProvider } from '@edx/frontend-platform/react';
+import { initializeMockApp } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import {
+ render,
+ screen,
+ fireEvent,
+ waitFor,
+} from '@testing-library/react';
+import { ContentLibrary, getContentLibraryApiUrl } from '../data/api';
+import initializeStore from '../../store';
+import { ToastProvider } from '../../generic/toast-context';
+import LibraryInfoHeader from './LibraryInfoHeader';
+
+let store;
+let axiosMock;
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+});
+
+const libraryData: ContentLibrary = {
+ id: 'lib:org1:lib1',
+ type: 'complex',
+ org: 'org1',
+ slug: 'lib1',
+ title: 'lib1',
+ description: 'lib1',
+ numBlocks: 2,
+ version: 0,
+ lastPublished: null,
+ lastDraftCreated: '2024-07-22',
+ publishedBy: 'staff',
+ lastDraftCreatedBy: 'staff',
+ allowLti: false,
+ allowPublicLearning: false,
+ allowPublicRead: false,
+ hasUnpublishedChanges: true,
+ hasUnpublishedDeletes: false,
+ canEditLibrary: true,
+ license: '',
+ created: '2024-06-26',
+ updated: '2024-07-20',
+};
+
+interface WrapperProps {
+ data: ContentLibrary,
+}
+
+const RootWrapper = ({ data } : WrapperProps) => (
+
+
+
+
+
+
+
+
+
+);
+
+describe('', () => {
+ beforeEach(() => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: true,
+ roles: [],
+ },
+ });
+ store = initializeStore();
+ axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ axiosMock.restore();
+ });
+
+ it('should render Library info Header', () => {
+ render();
+
+ expect(screen.getByText(libraryData.title)).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /edit library name/i })).toBeInTheDocument();
+ });
+
+ it('should not render edit title button without permission', () => {
+ const data = {
+ ...libraryData,
+ canEditLibrary: false,
+ };
+
+ render();
+
+ expect(screen.queryByRole('button', { name: /edit library name/i })).not.toBeInTheDocument();
+ });
+
+ it('should edit library title', async () => {
+ queryClient.getQueriesData = jest.fn().mockReturnValue([[null, { id: 1, title: 'Old Title' }]]);
+ const url = getContentLibraryApiUrl(libraryData.id);
+ axiosMock.onPatch(url).reply(200);
+ render();
+
+ const editTitleButton = screen.getByRole('button', { name: /edit library name/i });
+ fireEvent.click(editTitleButton);
+
+ const textBox = screen.getByRole('textbox', { name: /title input/i });
+
+ fireEvent.change(textBox, { target: { value: 'New Library Title' } });
+ fireEvent.keyDown(textBox, { key: 'Enter', code: 'Enter', charCode: 13 });
+
+ expect(textBox).not.toBeInTheDocument();
+ expect(await screen.findByText('Library updated successfully')).toBeInTheDocument();
+
+ await waitFor(() => expect(axiosMock.history.patch[0].url).toEqual(url));
+ });
+
+ it('should close edit library title on press Escape', async () => {
+ const url = getContentLibraryApiUrl(libraryData.id);
+ axiosMock.onPatch(url).reply(200);
+ render();
+
+ const editTitleButton = screen.getByRole('button', { name: /edit library name/i });
+ fireEvent.click(editTitleButton);
+
+ const textBox = screen.getByRole('textbox', { name: /title input/i });
+ fireEvent.keyDown(textBox, { key: 'Escape', code: 'Escape', charCode: 27 });
+
+ expect(textBox).not.toBeInTheDocument();
+
+ await waitFor(() => expect(axiosMock.history.patch.length).toEqual(0));
+ });
+
+ it('should show error on edit library tittle', async () => {
+ const url = getContentLibraryApiUrl(libraryData.id);
+ axiosMock.onPatch(url).reply(500);
+ render();
+
+ const editTitleButton = screen.getByRole('button', { name: /edit library name/i });
+ fireEvent.click(editTitleButton);
+
+ const textBox = screen.getByRole('textbox', { name: /title input/i });
+
+ fireEvent.change(textBox, { target: { value: 'New Library Title' } });
+ fireEvent.keyDown(textBox, { key: 'Enter', code: 'Enter', charCode: 13 });
+
+ expect(await screen.findByText('There was an error updating the library')).toBeInTheDocument();
+
+ await waitFor(() => expect(axiosMock.history.patch[0].url).toEqual(url));
+ });
+});
diff --git a/src/library-authoring/library-info/LibraryInfoHeader.tsx b/src/library-authoring/library-info/LibraryInfoHeader.tsx
new file mode 100644
index 000000000..e10fe2ec6
--- /dev/null
+++ b/src/library-authoring/library-info/LibraryInfoHeader.tsx
@@ -0,0 +1,86 @@
+import React, { useState, useContext } from 'react';
+import {
+ Icon,
+ IconButton,
+ Stack,
+ Form,
+} from '@openedx/paragon';
+import { Edit } from '@openedx/paragon/icons';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import messages from './messages';
+import { ContentLibrary } from '../data/api';
+import { useUpdateLibraryMetadata } from '../data/apiHooks';
+import { ToastContext } from '../../generic/toast-context';
+
+type LibraryInfoHeaderProps = {
+ library: ContentLibrary,
+};
+
+const LibraryInfoHeader = ({ library } : LibraryInfoHeaderProps) => {
+ const intl = useIntl();
+ const [inputIsActive, setIsActive] = useState(false);
+ const updateMutation = useUpdateLibraryMetadata();
+ const { showToast } = useContext(ToastContext);
+
+ const handleSaveTitle = (event) => {
+ const newTitle = event.target.value;
+ if (newTitle && newTitle !== library.title) {
+ updateMutation.mutateAsync({
+ id: library.id,
+ title: newTitle,
+ }).then(() => {
+ showToast(intl.formatMessage(messages.updateLibrarySuccessMsg));
+ }).catch(() => {
+ showToast(intl.formatMessage(messages.updateLibraryErrorMsg));
+ });
+ }
+ setIsActive(false);
+ };
+
+ const handleClick = () => {
+ setIsActive(true);
+ };
+
+ const hanldeOnKeyDown = (event) => {
+ if (event.key === 'Enter') {
+ handleSaveTitle(event);
+ } else if (event.key === 'Escape') {
+ setIsActive(false);
+ }
+ };
+
+ return (
+
+ { inputIsActive
+ ? (
+
+ )
+ : (
+ <>
+
+ {library.title}
+
+ {library.canEditLibrary && (
+
+ )}
+ >
+ )}
+
+ );
+};
+
+export default LibraryInfoHeader;
diff --git a/src/library-authoring/library-info/LibraryPublishStatus.scss b/src/library-authoring/library-info/LibraryPublishStatus.scss
new file mode 100644
index 000000000..9b920eea9
--- /dev/null
+++ b/src/library-authoring/library-info/LibraryPublishStatus.scss
@@ -0,0 +1,11 @@
+.library-publish-status {
+ &.draft-status {
+ background-color: #FDF3E9;
+ border-top: 4px solid #F4B57B;
+ }
+
+ &.published-status {
+ background-color: $info-100;
+ border-top: 4px solid $info-400;
+ }
+}
diff --git a/src/library-authoring/library-info/LibraryPublishStatus.tsx b/src/library-authoring/library-info/LibraryPublishStatus.tsx
new file mode 100644
index 000000000..e49704265
--- /dev/null
+++ b/src/library-authoring/library-info/LibraryPublishStatus.tsx
@@ -0,0 +1,171 @@
+import React, { useCallback, useContext, useMemo } from 'react';
+import classNames from 'classnames';
+import { Button, Container, Stack } from '@openedx/paragon';
+import { FormattedDate, FormattedTime, useIntl } from '@edx/frontend-platform/i18n';
+import { useCommitLibraryChanges } from '../data/apiHooks';
+import { ContentLibrary } from '../data/api';
+import { ToastContext } from '../../generic/toast-context';
+import messages from './messages';
+
+type LibraryPublishStatusProps = {
+ library: ContentLibrary,
+};
+
+const LibraryPublishStatus = ({ library } : LibraryPublishStatusProps) => {
+ const intl = useIntl();
+ const commitLibraryChanges = useCommitLibraryChanges();
+ const { showToast } = useContext(ToastContext);
+
+ const commit = useCallback(() => {
+ commitLibraryChanges.mutateAsync(library.id)
+ .then(() => {
+ showToast(intl.formatMessage(messages.publishSuccessMsg));
+ }).catch(() => {
+ showToast(intl.formatMessage(messages.publishErrorMsg));
+ });
+ }, []);
+
+ /**
+ * TODO, the discard changes breaks the library.
+ * Discomment this when discard changes is fixed.
+ const revert = useCallback(() => {
+ revertLibraryChanges.mutateAsync(library.id)
+ .then(() => {
+ showToast(intl.formatMessage(messages.revertSuccessMsg));
+ }).catch(() => {
+ showToast(intl.formatMessage(messages.revertErrorMsg));
+ });
+ }, []);
+ */
+
+ const {
+ isPublished,
+ statusMessage,
+ extraStatusMessage,
+ bodyMessage,
+ } = useMemo(() => {
+ let isPublishedResult: boolean;
+ let statusMessageResult : string;
+ let extraStatusMessageResult : string | undefined;
+ let bodyMessageResult : string | undefined;
+
+ const buildDate = ((date : string) => (
+
+
+
+ ));
+
+ const buildTime = ((date: string) => (
+
+
+
+ ));
+
+ const buildDraftBodyMessage = (() => {
+ if (library.lastDraftCreatedBy && library.lastDraftCreated) {
+ return intl.formatMessage(messages.lastDraftMsg, {
+ date: buildDate(library.lastDraftCreated),
+ time: buildTime(library.lastDraftCreated),
+ user: {library.lastDraftCreatedBy},
+ });
+ }
+ if (library.lastDraftCreated) {
+ return intl.formatMessage(messages.lastDraftMsgWithoutUser, {
+ date: buildDate(library.lastDraftCreated),
+ time: buildTime(library.lastDraftCreated),
+ });
+ }
+ if (library.created) {
+ return intl.formatMessage(messages.lastDraftMsgWithoutUser, {
+ date: buildDate(library.created),
+ time: buildTime(library.created),
+ });
+ }
+ return '';
+ });
+
+ if (!library.lastPublished) {
+ // Library is never published (new)
+ isPublishedResult = false;
+ statusMessageResult = intl.formatMessage(messages.draftStatusLabel);
+ extraStatusMessageResult = intl.formatMessage(messages.neverPublishedLabel);
+ bodyMessageResult = buildDraftBodyMessage();
+ } else if (library.hasUnpublishedChanges || library.hasUnpublishedDeletes) {
+ // Library is on Draft state
+ isPublishedResult = false;
+ statusMessageResult = intl.formatMessage(messages.draftStatusLabel);
+ extraStatusMessageResult = intl.formatMessage(messages.unpublishedStatusLabel);
+ bodyMessageResult = buildDraftBodyMessage();
+ } else {
+ // Library is published
+ isPublishedResult = true;
+ statusMessageResult = intl.formatMessage(messages.publishedStatusLabel);
+ if (library.publishedBy) {
+ bodyMessageResult = intl.formatMessage(messages.lastPublishedMsg, {
+ date: buildDate(library.lastPublished),
+ time: buildTime(library.lastPublished),
+ user: {library.publishedBy},
+ });
+ } else {
+ bodyMessageResult = intl.formatMessage(messages.lastPublishedMsgWithoutUser, {
+ date: buildDate(library.lastPublished),
+ time: buildTime(library.lastPublished),
+ });
+ }
+ }
+ return {
+ isPublished: isPublishedResult,
+ statusMessage: statusMessageResult,
+ extraStatusMessage: extraStatusMessageResult,
+ bodyMessage: bodyMessageResult,
+ };
+ }, [library]);
+
+ return (
+
+
+
+ {statusMessage}
+
+ { extraStatusMessage && (
+
+ {extraStatusMessage}
+
+ )}
+
+
+
+
+ {bodyMessage}
+
+
+ { /*
+ * TODO, the discard changes breaks the library.
+ * Discomment this when discard changes is fixed.
+
+
+
+ */ }
+
+
+
+ );
+};
+
+export default LibraryPublishStatus;
diff --git a/src/library-authoring/library-info/index.ts b/src/library-authoring/library-info/index.ts
new file mode 100644
index 000000000..5ed191c9d
--- /dev/null
+++ b/src/library-authoring/library-info/index.ts
@@ -0,0 +1,2 @@
+export { default as LibraryInfo } from './LibraryInfo';
+export { default as LibraryInfoHeader } from './LibraryInfoHeader';
diff --git a/src/library-authoring/library-info/messages.ts b/src/library-authoring/library-info/messages.ts
new file mode 100644
index 000000000..6b6b6dbac
--- /dev/null
+++ b/src/library-authoring/library-info/messages.ts
@@ -0,0 +1,111 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ editNameButtonAlt: {
+ id: 'course-authoring.library-authoring.sidebar.info.edit-name.alt',
+ defaultMessage: 'Edit library name',
+ description: 'Alt text for edit library name icon button',
+ },
+ organizationSectionTitle: {
+ id: 'course-authoring.library-authoring.sidebar.info.organization.title',
+ defaultMessage: 'Organization',
+ description: 'Title for Organization section in Library info sidebar.',
+ },
+ libraryHistorySectionTitle: {
+ id: 'course-authoring.library-authoring.sidebar.info.history.title',
+ defaultMessage: 'Library History',
+ description: 'Title for Library History section in Library info sidebar.',
+ },
+ lastModifiedLabel: {
+ id: 'course-authoring.library-authoring.sidebar.info.history.last-modified',
+ defaultMessage: 'Last Modified',
+ description: 'Last Modified label used in Library History section.',
+ },
+ createdLabel: {
+ id: 'course-authoring.library-authoring.sidebar.info.history.created',
+ defaultMessage: 'Created',
+ description: 'Created label used in Library History section.',
+ },
+ draftStatusLabel: {
+ id: 'course-authoring.library-authoring.sidebar.info.publish-status.draft',
+ defaultMessage: 'Draft',
+ description: 'Label in library info sidebar when the library is on draft status',
+ },
+ neverPublishedLabel: {
+ id: 'course-authoring.library-authoring.sidebar.info.publish-status.never',
+ defaultMessage: '(Never Published)',
+ description: 'Label in library info sidebar when the library is never published',
+ },
+ unpublishedStatusLabel: {
+ id: 'course-authoring.library-authoring.sidebar.info.publish-status.unpublished',
+ defaultMessage: '(Unpublished Changes)',
+ description: 'Label in library info sidebar when the library has unpublished changes',
+ },
+ publishedStatusLabel: {
+ id: 'course-authoring.library-authoring.sidebar.info.publish-status.published',
+ defaultMessage: 'Published',
+ description: 'Label in library info sidebar when the library is on published status',
+ },
+ publishButtonLabel: {
+ id: 'course-authoring.library-authoring.sidebar.info.publish-status.publish-button',
+ defaultMessage: 'Publish',
+ description: 'Label of publish button for a library.',
+ },
+ discardChangesButtonLabel: {
+ id: 'course-authoring.library-authoring.sidebar.info.publish-status.discard-button',
+ defaultMessage: 'Discard Changes',
+ description: 'Label of discard changes button for a library.',
+ },
+ lastPublishedMsg: {
+ id: 'course-authoring.library-authoring.sidebar.info.publish-status.last-published',
+ defaultMessage: 'Last published on {date} at {time} UTC by {user}.',
+ description: 'Body meesage of the library info sidebar when library is published.',
+ },
+ lastPublishedMsgWithoutUser: {
+ id: 'course-authoring.library-authoring.sidebar.info.publish-status.last-published-no-user',
+ defaultMessage: 'Last published on {date} at {time} UTC.',
+ description: 'Body meesage of the library info sidebar when library is published.',
+ },
+ lastDraftMsg: {
+ id: 'course-authoring.library-authoring.sidebar.info.publish-status.last-draft',
+ defaultMessage: 'Draft saved on {date} at {time} UTC by {user}.',
+ description: 'Body meesage of the library info sidebar when library is on draft status.',
+ },
+ lastDraftMsgWithoutUser: {
+ id: 'course-authoring.library-authoring.sidebar.info.publish-status.last-draft-no-user',
+ defaultMessage: 'Draft saved on {date} at {time} UTC.',
+ description: 'Body meesage of the library info sidebar when library is on draft status.',
+ },
+ publishSuccessMsg: {
+ id: 'course-authoring.library-authoring.publish.success',
+ defaultMessage: 'Library published successfully',
+ description: 'Message when the library is published successfully.',
+ },
+ publishErrorMsg: {
+ id: 'course-authoring.library-authoring.publish.error',
+ defaultMessage: 'There was an error publishing the library.',
+ description: 'Message when there is an error when publishing the library.',
+ },
+ revertSuccessMsg: {
+ id: 'course-authoring.library-authoring.revert.success',
+ defaultMessage: 'Library changes reverted successfully',
+ description: 'Message when the library changes are reverted successfully.',
+ },
+ revertErrorMsg: {
+ id: 'course-authoring.library-authoring.publish.error',
+ defaultMessage: 'There was an error reverting changes in the library.',
+ description: 'Message when there is an error when reverting changes in the library.',
+ },
+ updateLibrarySuccessMsg: {
+ id: 'course-authoring.library-authoring.library.update.success',
+ defaultMessage: 'Library updated successfully',
+ description: 'Message when the library is updated successfully',
+ },
+ updateLibraryErrorMsg: {
+ id: 'course-authoring.library-authoring.library.update.error',
+ defaultMessage: 'There was an error updating the library',
+ description: 'Message when there is an error when updating the library',
+ },
+});
+
+export default messages;
diff --git a/src/library-authoring/library-sidebar/LibrarySidebar.tsx b/src/library-authoring/library-sidebar/LibrarySidebar.tsx
index 734379f51..314de8792 100644
--- a/src/library-authoring/library-sidebar/LibrarySidebar.tsx
+++ b/src/library-authoring/library-sidebar/LibrarySidebar.tsx
@@ -7,8 +7,14 @@ import {
import { Close } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from '../messages';
-import { AddContentContainer } from '../add-content';
-import { LibraryContext } from '../common/context';
+import { AddContentContainer, AddContentHeader } from '../add-content';
+import { LibraryContext, SidebarBodyComponentId } from '../common/context';
+import { LibraryInfo, LibraryInfoHeader } from '../library-info';
+import { ContentLibrary } from '../data/api';
+
+type LibrarySidebarProps = {
+ library: ContentLibrary,
+};
/**
* Sidebar container for library pages.
@@ -19,23 +25,29 @@ import { LibraryContext } from '../common/context';
* You can add more components in `bodyComponentMap`.
* Use the slice actions to open and close this sidebar.
*/
-const LibrarySidebar = () => {
+const LibrarySidebar = ({ library }: LibrarySidebarProps) => {
const intl = useIntl();
const { sidebarBodyComponent, closeLibrarySidebar } = useContext(LibraryContext);
const bodyComponentMap = {
- 'add-content': ,
+ [SidebarBodyComponentId.AddContent]: ,
+ [SidebarBodyComponentId.Info]: ,
+ unknown: null,
+ };
+
+ const headerComponentMap = {
+ 'add-content': ,
+ info: ,
unknown: null,
};
const buildBody = () : React.ReactNode | null => bodyComponentMap[sidebarBodyComponent || 'unknown'];
+ const buildHeader = (): React.ReactNode | null => headerComponentMap[sidebarBodyComponent || 'unknown'];
return (
-
+
-
- {intl.formatMessage(messages.addContentTitle)}
-
+ {buildHeader()}
{
variant="black"
/>
- {buildBody()}
-
+
+ {buildBody()}
+
+
);
};
diff --git a/src/library-authoring/messages.ts b/src/library-authoring/messages.ts
index 38332b543..9a61dc9ed 100644
--- a/src/library-authoring/messages.ts
+++ b/src/library-authoring/messages.ts
@@ -100,6 +100,11 @@ const messages = defineMessages({
defaultMessage: 'Close',
description: 'Alt text of close button',
},
+ libraryInfoButton: {
+ id: 'course-authoring.library-authoring.buttons.library-info.text',
+ defaultMessage: 'Library Info',
+ description: 'Text of button to open "Library Info sidebar"',
+ },
readOnlyBadge: {
id: 'course-authoring.library-authoring.badge.read-only',
defaultMessage: 'Read Only',