diff --git a/src/library-authoring/component-info/ComponentInfo.tsx b/src/library-authoring/component-info/ComponentInfo.tsx
index 3e71570170..735257d732 100644
--- a/src/library-authoring/component-info/ComponentInfo.tsx
+++ b/src/library-authoring/component-info/ComponentInfo.tsx
@@ -11,6 +11,7 @@ import { Link } from 'react-router-dom';
import { getEditUrl } from '../components/utils';
import { ComponentMenu } from '../components';
import { ComponentDeveloperInfo } from './ComponentDeveloperInfo';
+import ComponentManagement from './ComponentManagement';
import ComponentPreview from './ComponentPreview';
import messages from './messages';
@@ -46,7 +47,7 @@ const ComponentInfo = ({ usageKey }: ComponentInfoProps) => {
- Manage tab placeholder
+
Details tab placeholder
diff --git a/src/library-authoring/component-info/ComponentInfoHeader.test.tsx b/src/library-authoring/component-info/ComponentInfoHeader.test.tsx
index b0033f32ae..0d8e644000 100644
--- a/src/library-authoring/component-info/ComponentInfoHeader.test.tsx
+++ b/src/library-authoring/component-info/ComponentInfoHeader.test.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import MockAdapter from 'axios-mock-adapter';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
diff --git a/src/library-authoring/component-info/ComponentManagement.test.tsx b/src/library-authoring/component-info/ComponentManagement.test.tsx
new file mode 100644
index 0000000000..b56d33bb0f
--- /dev/null
+++ b/src/library-authoring/component-info/ComponentManagement.test.tsx
@@ -0,0 +1,70 @@
+import { setConfig, getConfig } from '@edx/frontend-platform';
+
+import {
+ initializeMocks,
+ render,
+ screen,
+} from '../../testUtils';
+import { mockLibraryBlockMetadata } from '../data/api.mocks';
+import ComponentManagement from './ComponentManagement';
+
+/*
+ * FIXME: Summarize the reason here
+ * https://stackoverflow.com/questions/47902335/innertext-is-undefined-in-jest-test
+ */
+const getInnerText = (element: Element) => element?.textContent
+ ?.split('\n')
+ .filter((text) => text && !text.match(/^\s+$/))
+ .map((text) => text.trim())
+ .join(' ');
+
+const matchInnerText = (nodeName: string, textToMatch: string) => (_: string, element: Element) => (
+ element.nodeName === nodeName && getInnerText(element) === textToMatch
+);
+
+describe('', () => {
+ it('should render draft status', async () => {
+ initializeMocks();
+ mockLibraryBlockMetadata.applyMock();
+ render();
+ expect(await screen.findByText('Draft')).toBeInTheDocument();
+ expect(await screen.findByText('(Never Published)')).toBeInTheDocument();
+ expect(screen.getByText(matchInnerText('SPAN', 'Draft saved on June 20, 2024 at 13:54 UTC.'))).toBeInTheDocument();
+ });
+
+ it('should render published status', async () => {
+ initializeMocks();
+ mockLibraryBlockMetadata.applyMock();
+ render();
+ expect(await screen.findByText('Published')).toBeInTheDocument();
+ expect(screen.getByText('Published')).toBeInTheDocument();
+ expect(
+ screen.getByText(matchInnerText('SPAN', 'Last published on June 21, 2024 at 24:00 UTC by Luke.')),
+ ).toBeInTheDocument();
+ });
+
+ it('should render the tagging info', async () => {
+ setConfig({
+ ...getConfig(),
+ ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
+ });
+ initializeMocks();
+ mockLibraryBlockMetadata.applyMock();
+ render();
+ expect(await screen.findByText('Tags')).toBeInTheDocument();
+ // TODO: replace with actual data when implement tag list
+ expect(screen.queryByText('Tags placeholder')).toBeInTheDocument();
+ });
+
+ it('should not render draft status', async () => {
+ setConfig({
+ ...getConfig(),
+ ENABLE_TAGGING_TAXONOMY_PAGES: 'false',
+ });
+ initializeMocks();
+ mockLibraryBlockMetadata.applyMock();
+ render();
+ expect(await screen.findByText('Draft')).toBeInTheDocument();
+ expect(screen.queryByText('Tags')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/library-authoring/component-info/ComponentManagement.tsx b/src/library-authoring/component-info/ComponentManagement.tsx
new file mode 100644
index 0000000000..172f8331da
--- /dev/null
+++ b/src/library-authoring/component-info/ComponentManagement.tsx
@@ -0,0 +1,57 @@
+import { getConfig } from '@edx/frontend-platform';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Collapsible, Icon, Stack } from '@openedx/paragon';
+import { Tag } from '@openedx/paragon/icons';
+
+import { useLibraryBlockMetadata } from '../data/apiHooks';
+import StatusWidget from '../generic/status-widget';
+import messages from './messages';
+
+interface ComponentManagementProps {
+ usageKey: string;
+}
+const ComponentManagement = ({ usageKey }: ComponentManagementProps) => {
+ const intl = useIntl();
+ const { data: componentMetadata } = useLibraryBlockMetadata(usageKey);
+
+ if (!componentMetadata) {
+ return null;
+ }
+
+ return (
+
+
+ {[true, 'true'].includes(getConfig().ENABLE_TAGGING_TAXONOMY_PAGES)
+ && (
+
+
+ {intl.formatMessage(messages.manageTabTagsTitle)}
+
+ )}
+ className="border-0"
+ >
+ Tags placeholder
+
+ )}
+
+
+ {intl.formatMessage(messages.manageTabCollectionsTitle)}
+
+ )}
+ className="border-0"
+ >
+ Collections placeholder
+
+
+ );
+};
+
+export default ComponentManagement;
diff --git a/src/library-authoring/component-info/messages.ts b/src/library-authoring/component-info/messages.ts
index e945064cee..8a9404081e 100644
--- a/src/library-authoring/component-info/messages.ts
+++ b/src/library-authoring/component-info/messages.ts
@@ -36,6 +36,16 @@ const messages = defineMessages({
defaultMessage: 'Manage',
description: 'Title for manage tab',
},
+ manageTabTagsTitle: {
+ id: 'course-authoring.library-authoring.component.manage-tab.tags-title',
+ defaultMessage: 'Tags',
+ description: 'Title for the Tags container in the management tab',
+ },
+ manageTabCollectionsTitle: {
+ id: 'course-authoring.library-authoring.component.manage-tab.collections-title',
+ defaultMessage: 'Collections',
+ description: 'Title for the Collections container in the management tab',
+ },
detailsTabTitle: {
id: 'course-authoring.library-authoring.component.details-tab.title',
defaultMessage: 'Details',
diff --git a/src/library-authoring/components/LibraryComponents.test.tsx b/src/library-authoring/components/LibraryComponents.test.tsx
index 695b56f04b..fd45dac12c 100644
--- a/src/library-authoring/components/LibraryComponents.test.tsx
+++ b/src/library-authoring/components/LibraryComponents.test.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
diff --git a/src/library-authoring/data/api.mocks.ts b/src/library-authoring/data/api.mocks.ts
index 6b35bac8b5..480e78d661 100644
--- a/src/library-authoring/data/api.mocks.ts
+++ b/src/library-authoring/data/api.mocks.ts
@@ -126,16 +126,26 @@ mockCreateLibraryBlock.newHtmlData = {
blockType: 'html',
displayName: 'New Text Component',
hasUnpublishedChanges: true,
+ lastPublished: null, // or e.g. '2024-08-30T16:37:42Z',
+ publishedBy: null, // or e.g. 'test_author',
+ lastDraftCreated: '2024-07-22T21:37:49Z',
+ lastDraftCreatedBy: null,
+ created: '2024-07-22T21:37:49Z',
tagsCount: 0,
-} satisfies api.CreateBlockDataResponse;
+} satisfies api.LibraryBlockMetadata;
mockCreateLibraryBlock.newProblemData = {
id: 'lb:Axim:TEST:problem:prob1',
defKey: 'prob1',
blockType: 'problem',
displayName: 'New Problem',
hasUnpublishedChanges: true,
+ lastPublished: null, // or e.g. '2024-08-30T16:37:42Z',
+ publishedBy: null, // or e.g. 'test_author',
+ lastDraftCreated: '2024-07-22T21:37:49Z',
+ lastDraftCreatedBy: null,
+ created: '2024-07-22T21:37:49Z',
tagsCount: 0,
-} satisfies api.CreateBlockDataResponse;
+} satisfies api.LibraryBlockMetadata;
/** Apply this mock. Returns a spy object that can tell you if it's been called. */
mockCreateLibraryBlock.applyMock = () => (
jest.spyOn(api, 'createLibraryBlock').mockImplementation(mockCreateLibraryBlock)
@@ -172,3 +182,49 @@ mockXBlockFields.dataNewHtml = {
} satisfies api.XBlockFields;
/** Apply this mock. Returns a spy object that can tell you if it's been called. */
mockXBlockFields.applyMock = () => jest.spyOn(api, 'getXBlockFields').mockImplementation(mockXBlockFields);
+
+/**
+ * Mock for `getLibraryBlockMetadata()`
+ *
+ * This mock returns different data/responses depending on the ID of the block
+ * that you request. Use `mockLibraryBlockMetadata.applyMock()` to apply it to the whole
+ * test suite.
+ */
+export async function mockLibraryBlockMetadata(usageKey: string): Promise {
+ const thisMock = mockLibraryBlockMetadata;
+ switch (usageKey) {
+ case thisMock.usageKeyNeverPublished: return thisMock.dataNeverPublished;
+ case thisMock.usageKeyPublished: return thisMock.dataPublished;
+ default: throw new Error(`No mock has been set up for usageKey "${usageKey}"`);
+ }
+}
+mockLibraryBlockMetadata.usageKeyNeverPublished = 'lb:Axim:TEST1:html:571fe018-f3ce-45c9-8f53-5dafcb422fd1';
+mockLibraryBlockMetadata.dataNeverPublished = {
+ id: 'lb:Axim:TEST1:html:571fe018-f3ce-45c9-8f53-5dafcb422fd1',
+ defKey: null,
+ blockType: 'html',
+ displayName: 'Introduction to Testing 1',
+ lastPublished: null,
+ publishedBy: null,
+ lastDraftCreated: null,
+ lastDraftCreatedBy: null,
+ hasUnpublishedChanges: false,
+ created: '2024-06-20T13:54:21Z',
+ tagsCount: 0,
+} satisfies api.LibraryBlockMetadata;
+mockLibraryBlockMetadata.usageKeyPublished = 'lb:Axim:TEST2:html:571fe018-f3ce-45c9-8f53-5dafcb422fd2';
+mockLibraryBlockMetadata.dataPublished = {
+ id: 'lb:Axim:TEST2:html:571fe018-f3ce-45c9-8f53-5dafcb422fd2',
+ defKey: null,
+ blockType: 'html',
+ displayName: 'Introduction to Testing 2',
+ lastPublished: '2024-06-21T00:00:00',
+ publishedBy: 'Luke',
+ lastDraftCreated: null,
+ lastDraftCreatedBy: '2024-06-20T20:00:00Z',
+ hasUnpublishedChanges: false,
+ created: '2024-06-20T13:54:21Z',
+ tagsCount: 0,
+} satisfies api.LibraryBlockMetadata;
+/** Apply this mock. Returns a spy object that can tell you if it's been called. */
+mockLibraryBlockMetadata.applyMock = () => jest.spyOn(api, 'getLibraryBlockMetadata').mockImplementation(mockLibraryBlockMetadata);
diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts
index da2d80390e..76388b1641 100644
--- a/src/library-authoring/data/api.ts
+++ b/src/library-authoring/data/api.ts
@@ -18,6 +18,14 @@ export const getLibraryBlockTypesUrl = (libraryId: string) => `${getApiBaseUrl()
*/
export const getCreateLibraryBlockUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/blocks/`;
+/**
+ * Get the URL for library block metadata.
+ */
+export const getLibraryBlockMetadataUrl = (usageKey: string) => `${getApiBaseUrl()}/api/libraries/v2/blocks/${usageKey}/`;
+
+/**
+ * Get the URL for content library list API.
+ */
export const getContentLibraryV2ListApiUrl = () => `${getApiBaseUrl()}/api/libraries/v2/`;
/**
@@ -110,12 +118,17 @@ export interface CreateBlockDataRequest {
definitionId: string;
}
-export interface CreateBlockDataResponse {
+export interface LibraryBlockMetadata {
id: string;
blockType: string;
defKey: string | null;
displayName: string;
+ lastPublished: string | null;
+ publishedBy: string | null,
+ lastDraftCreated: string | null,
+ lastDraftCreatedBy: string | null,
hasUnpublishedChanges: boolean;
+ created: string | null,
tagsCount: number;
}
@@ -166,7 +179,7 @@ export async function createLibraryBlock({
libraryId,
blockType,
definitionId,
-}: CreateBlockDataRequest): Promise {
+}: CreateBlockDataRequest): Promise {
const client = getAuthenticatedHttpClient();
const { data } = await client.post(
getCreateLibraryBlockUrl(libraryId),
@@ -229,7 +242,7 @@ export async function revertLibraryChanges(libraryId: string) {
export async function libraryPasteClipboard({
libraryId,
blockId,
-}: LibraryPasteClipboardRequest): Promise {
+}: LibraryPasteClipboardRequest): Promise {
const client = getAuthenticatedHttpClient();
const { data } = await client.post(
getLibraryPasteClipboardUrl(libraryId),
@@ -240,6 +253,14 @@ export async function libraryPasteClipboard({
return data;
}
+/**
+ * Fetch library block metadata.
+ */
+export async function getLibraryBlockMetadata(usageKey: string): Promise {
+ const { data } = await getAuthenticatedHttpClient().get(getLibraryBlockMetadataUrl(usageKey));
+ return camelCaseObject(data);
+}
+
/**
* Fetch xblock fields.
*/
diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts
index 656ac21f63..91b07c6a7f 100644
--- a/src/library-authoring/data/apiHooks.ts
+++ b/src/library-authoring/data/apiHooks.ts
@@ -21,6 +21,7 @@ import {
revertLibraryChanges,
updateLibraryMetadata,
libraryPasteClipboard,
+ getLibraryBlockMetadata,
getXBlockFields,
updateXBlockFields,
createCollection,
@@ -72,6 +73,7 @@ export const xblockQueryKeys = {
xblockFields: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'fields'],
/** OLX (XML representation of the fields/content) */
xblockOLX: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'OLX'],
+ componentMetadata: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'componentMetadata'],
};
/**
@@ -197,6 +199,13 @@ export const useLibraryPasteClipboard = () => {
});
};
+export const useLibraryBlockMetadata = (usageId: string) => (
+ useQuery({
+ queryKey: xblockQueryKeys.componentMetadata(usageId),
+ queryFn: () => getLibraryBlockMetadata(usageId),
+ })
+);
+
export const useXBlockFields = (usageKey: string) => (
useQuery({
queryKey: xblockQueryKeys.xblockFields(usageKey),
diff --git a/src/library-authoring/generic/index.scss b/src/library-authoring/generic/index.scss
new file mode 100644
index 0000000000..8e15b4671b
--- /dev/null
+++ b/src/library-authoring/generic/index.scss
@@ -0,0 +1 @@
+@import "./status-widget/StatusWidget";
diff --git a/src/library-authoring/library-info/LibraryPublishStatus.scss b/src/library-authoring/generic/status-widget/StatusWidget.scss
similarity index 87%
rename from src/library-authoring/library-info/LibraryPublishStatus.scss
rename to src/library-authoring/generic/status-widget/StatusWidget.scss
index 9b920eea90..cdb1c4cf61 100644
--- a/src/library-authoring/library-info/LibraryPublishStatus.scss
+++ b/src/library-authoring/generic/status-widget/StatusWidget.scss
@@ -1,4 +1,4 @@
-.library-publish-status {
+.status-widget {
&.draft-status {
background-color: #FDF3E9;
border-top: 4px solid #F4B57B;
@@ -9,3 +9,4 @@
border-top: 4px solid $info-400;
}
}
+
diff --git a/src/library-authoring/generic/status-widget/index.tsx b/src/library-authoring/generic/status-widget/index.tsx
new file mode 100644
index 0000000000..19fdc71444
--- /dev/null
+++ b/src/library-authoring/generic/status-widget/index.tsx
@@ -0,0 +1,207 @@
+import {
+ FormattedDate,
+ FormattedMessage,
+ FormattedTime,
+ useIntl,
+} from '@edx/frontend-platform/i18n';
+import { Button, Container, Stack } from '@openedx/paragon';
+import classNames from 'classnames';
+
+import messages from './messages';
+
+const CustomFormattedDate = ({ date }: { date: string }) => (
+
+
+
+);
+
+const CustomFormattedTime = ({ date }: { date: string }) => (
+
+
+
+);
+
+type DraftBodyMessageProps = {
+ lastDraftCreatedBy: string | null;
+ lastDraftCreated: string | null;
+ created: string | null;
+};
+const DraftBodyMessage = ({ lastDraftCreatedBy, lastDraftCreated, created }: DraftBodyMessageProps) => {
+ if (lastDraftCreatedBy && lastDraftCreated) {
+ return (
+ ,
+ time: ,
+ user: {lastDraftCreatedBy},
+ }}
+ />
+ );
+ }
+
+ if (lastDraftCreated) {
+ return (
+ ,
+ time: ,
+ }}
+ />
+ );
+ }
+
+ if (created) {
+ return (
+ ,
+ time: ,
+ }}
+ />
+ );
+ }
+
+ return null;
+};
+
+type StatusWidgedProps = {
+ lastPublished: string | null;
+ hasUnpublishedChanges: boolean;
+ hasUnpublishedDeletes?: boolean;
+ lastDraftCreatedBy: string | null;
+ lastDraftCreated: string | null;
+ created: string | null;
+ publishedBy: string | null;
+ numBlocks?: number;
+ onCommit?: () => void;
+ onRevert?: () => void;
+};
+
+/**
+ * This component displays the status of an entity (published, draft, etc.) and allows the user to publish
+ * or discard changes.
+ *
+ * This component doesn't handle fetching the data or any other side effects. It only displays the status
+ * and provides the buttons to commit or revert changes.
+ *
+ * @example
+ * ```tsx
+ * const { data: libraryData } = useContentLibrary(libraryId);
+ * const onCommit = () => { commitChanges(libraryId); };
+ * const onRevert = () => { revertChanges(libraryId); };
+ *
+ * return ;
+ * ```
+ */
+const StatusWidget = ({
+ lastPublished,
+ hasUnpublishedChanges,
+ hasUnpublishedDeletes,
+ lastDraftCreatedBy,
+ lastDraftCreated,
+ created,
+ publishedBy,
+ numBlocks,
+ onCommit,
+ onRevert,
+}: StatusWidgedProps) => {
+ const intl = useIntl();
+
+ let isNew: boolean | undefined;
+ let isPublished: boolean;
+ let statusMessage: string;
+ let extraStatusMessage: string | undefined;
+ let bodyMessage: React.ReactNode | undefined;
+
+ if (!lastPublished) {
+ // Entity is never published (new)
+ isNew = numBlocks != null && numBlocks === 0; // allow discarding if components are added
+ isPublished = false;
+ statusMessage = intl.formatMessage(messages.draftStatusLabel);
+ extraStatusMessage = intl.formatMessage(messages.neverPublishedLabel);
+ bodyMessage = ();
+ } else if (hasUnpublishedChanges || hasUnpublishedDeletes) {
+ // Entity is on Draft state
+ isPublished = false;
+ statusMessage = intl.formatMessage(messages.draftStatusLabel);
+ extraStatusMessage = intl.formatMessage(messages.unpublishedStatusLabel);
+ bodyMessage = ();
+ } else {
+ // Entity is published
+ isPublished = true;
+ statusMessage = intl.formatMessage(messages.publishedStatusLabel);
+ if (publishedBy) {
+ bodyMessage = (
+ ,
+ time: ,
+ user: {publishedBy},
+ }}
+ />
+ );
+ } else {
+ bodyMessage = (
+ ,
+ time: ,
+ }}
+ />
+ );
+ }
+ }
+
+ return (
+
+
+
+ {statusMessage}
+
+ { extraStatusMessage && (
+
+ {extraStatusMessage}
+
+ )}
+
+
+
+
+ {bodyMessage}
+
+ {onCommit && (
+
+ )}
+ {onRevert && (
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default StatusWidget;
diff --git a/src/library-authoring/generic/status-widget/messages.ts b/src/library-authoring/generic/status-widget/messages.ts
new file mode 100644
index 0000000000..5ac3b60745
--- /dev/null
+++ b/src/library-authoring/generic/status-widget/messages.ts
@@ -0,0 +1,56 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ draftStatusLabel: {
+ id: 'course-authoring.library-authoring.generic.status-widget.draft',
+ defaultMessage: 'Draft',
+ description: 'Label in library authoring sidebar when the entity is on draft status',
+ },
+ neverPublishedLabel: {
+ id: 'course-authoring.library-authoring.generic.status-widget.never',
+ defaultMessage: '(Never Published)',
+ description: 'Label in library authoring sidebar when the entity is never published',
+ },
+ unpublishedStatusLabel: {
+ id: 'course-authoring.library-authoring.generic.status-widget.unpublished',
+ defaultMessage: '(Unpublished Changes)',
+ description: 'Label in library authoring sidebar when the entity has unpublished changes',
+ },
+ publishedStatusLabel: {
+ id: 'course-authoring.library-authoring.generic.status-widget.published',
+ defaultMessage: 'Published',
+ description: 'Label in library authoring sidebar when the entity is on published status',
+ },
+ lastPublishedMsg: {
+ id: 'course-authoring.library-authoring.generic.status-widget.last-published',
+ defaultMessage: 'Last published on {date} at {time} UTC by {user}.',
+ description: 'Body message of the library authoring sidebar when the entity is published.',
+ },
+ lastPublishedMsgWithoutUser: {
+ id: 'course-authoring.library-authoring.generic.status-widget.last-published-no-user',
+ defaultMessage: 'Last published on {date} at {time} UTC.',
+ description: 'Body message of the library authoring sidebar when the entity is published.',
+ },
+ lastDraftMsg: {
+ id: 'course-authoring.library-authoring.generic.status-widget.last-draft',
+ defaultMessage: 'Draft saved on {date} at {time} UTC by {user}.',
+ description: 'Body message of the library authoring sidebar when the entity is on draft status.',
+ },
+ lastDraftMsgWithoutUser: {
+ id: 'course-authoring.library-authoring.generic.status-widget.last-draft-no-user',
+ defaultMessage: 'Draft saved on {date} at {time} UTC.',
+ description: 'Body message of the library authoring sidebar when the entity is on draft status.',
+ },
+ publishButtonLabel: {
+ id: 'course-authoring.library-authoring.generic.status-widget.publish-button',
+ defaultMessage: 'Publish',
+ description: 'Label of publish button for an entity.',
+ },
+ discardChangesButtonLabel: {
+ id: 'course-authoring.library-authoring.generic.status-widget.discard-button',
+ defaultMessage: 'Discard Changes',
+ description: 'Label of discard changes button for an entity.',
+ },
+});
+
+export default messages;
diff --git a/src/library-authoring/index.scss b/src/library-authoring/index.scss
index a82de4942f..7bfb8cae9f 100644
--- a/src/library-authoring/index.scss
+++ b/src/library-authoring/index.scss
@@ -1,4 +1,4 @@
-@import "library-authoring/component-info/ComponentPreview";
-@import "library-authoring/components/ComponentCard";
-@import "library-authoring/library-info/LibraryPublishStatus";
-@import "library-authoring/LibraryAuthoringPage";
+@import "./component-info/ComponentPreview";
+@import "./components/ComponentCard";
+@import "./generic";
+@import "./LibraryAuthoringPage";
\ No newline at end of file
diff --git a/src/library-authoring/library-info/LibraryPublishStatus.tsx b/src/library-authoring/library-info/LibraryPublishStatus.tsx
index e0602e4724..0765b0e04f 100644
--- a/src/library-authoring/library-info/LibraryPublishStatus.tsx
+++ b/src/library-authoring/library-info/LibraryPublishStatus.tsx
@@ -1,10 +1,10 @@
-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 React, { useCallback, useContext } from 'react';
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { ToastContext } from '../../generic/toast-context';
+import StatusWidget from '../generic/status-widget';
import { useCommitLibraryChanges, useRevertLibraryChanges } from '../data/apiHooks';
import { ContentLibrary } from '../data/api';
-import { ToastContext } from '../../generic/toast-context';
import messages from './messages';
type LibraryPublishStatusProps = {
@@ -35,133 +35,12 @@ const LibraryPublishStatus = ({ library } : LibraryPublishStatusProps) => {
});
}, []);
- const {
- isPublished,
- isNew,
- statusMessage,
- extraStatusMessage,
- bodyMessage,
- } = useMemo(() => {
- let isPublishedResult: boolean;
- let isNewResult = false;
- let statusMessageResult : string;
- let extraStatusMessageResult : string | undefined;
- let bodyMessageResult : React.ReactNode | 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)
- isNewResult = library.numBlocks === 0; // allow discarding if components are added
- 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,
- isNew: isNewResult,
- statusMessage: statusMessageResult,
- extraStatusMessage: extraStatusMessageResult,
- bodyMessage: bodyMessageResult,
- };
- }, [library]);
-
return (
-
-
-
- {statusMessage}
-
- { extraStatusMessage && (
-
- {extraStatusMessage}
-
- )}
-
-
-
-
- {bodyMessage}
-
-
-
-
-
-
-
-
+
);
};
diff --git a/src/library-authoring/library-info/messages.ts b/src/library-authoring/library-info/messages.ts
index 6b6b6dbac6..1be61a8ebd 100644
--- a/src/library-authoring/library-info/messages.ts
+++ b/src/library-authoring/library-info/messages.ts
@@ -26,56 +26,6 @@ const messages = defineMessages({
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',
diff --git a/src/library-authoring/library-sidebar/LibrarySidebar.tsx b/src/library-authoring/library-sidebar/LibrarySidebar.tsx
index 30b3c43abc..6ced6ba50e 100644
--- a/src/library-authoring/library-sidebar/LibrarySidebar.tsx
+++ b/src/library-authoring/library-sidebar/LibrarySidebar.tsx
@@ -21,10 +21,10 @@ type LibrarySidebarProps = {
* Sidebar container for library pages.
*
* It's designed to "squash" the page when open.
- * Uses `sidebarBodyComponent` of the `store` to
+ * Uses `sidebarBodyComponent` of the `context` to
* choose which component is rendered.
* You can add more components in `bodyComponentMap`.
- * Use the slice actions to open and close this sidebar.
+ * Use the returned actions to open and close this sidebar.
*/
const LibrarySidebar = ({ library }: LibrarySidebarProps) => {
const intl = useIntl();