feat: add component sidebar manage tab [FC-0062] (#1275)

This commit is contained in:
Rômulo Penido
2024-09-16 16:13:41 -03:00
committed by GitHub
parent 902853d649
commit dd7e4d4297
17 changed files with 512 additions and 196 deletions

View File

@@ -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) => {
<ComponentPreview usageKey={usageKey} />
</Tab>
<Tab eventKey="manage" title={intl.formatMessage(messages.manageTabTitle)}>
Manage tab placeholder
<ComponentManagement usageKey={usageKey} />
</Tab>
<Tab eventKey="details" title={intl.formatMessage(messages.detailsTabTitle)}>
Details tab placeholder

View File

@@ -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';

View File

@@ -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('<ComponentManagement />', () => {
it('should render draft status', async () => {
initializeMocks();
mockLibraryBlockMetadata.applyMock();
render(<ComponentManagement usageKey={mockLibraryBlockMetadata.usageKeyNeverPublished} />);
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(<ComponentManagement usageKey={mockLibraryBlockMetadata.usageKeyPublished} />);
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(<ComponentManagement usageKey={mockLibraryBlockMetadata.usageKeyNeverPublished} />);
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(<ComponentManagement usageKey={mockLibraryBlockMetadata.usageKeyNeverPublished} />);
expect(await screen.findByText('Draft')).toBeInTheDocument();
expect(screen.queryByText('Tags')).not.toBeInTheDocument();
});
});

View File

@@ -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 (
<Stack gap={3}>
<StatusWidget
{...componentMetadata}
/>
{[true, 'true'].includes(getConfig().ENABLE_TAGGING_TAXONOMY_PAGES)
&& (
<Collapsible
defaultOpen
title={(
<Stack gap={1} direction="horizontal">
<Icon src={Tag} />
{intl.formatMessage(messages.manageTabTagsTitle)}
</Stack>
)}
className="border-0"
>
Tags placeholder
</Collapsible>
)}
<Collapsible
defaultOpen
title={(
<Stack gap={1} direction="horizontal">
<Icon src={Tag} />
{intl.formatMessage(messages.manageTabCollectionsTitle)}
</Stack>
)}
className="border-0"
>
Collections placeholder
</Collapsible>
</Stack>
);
};
export default ComponentManagement;

View File

@@ -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',

View File

@@ -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';

View File

@@ -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<api.LibraryBlockMetadata> {
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);

View File

@@ -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<CreateBlockDataResponse> {
}: CreateBlockDataRequest): Promise<LibraryBlockMetadata> {
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<CreateBlockDataResponse> {
}: LibraryPasteClipboardRequest): Promise<LibraryBlockMetadata> {
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<LibraryBlockMetadata> {
const { data } = await getAuthenticatedHttpClient().get(getLibraryBlockMetadataUrl(usageKey));
return camelCaseObject(data);
}
/**
* Fetch xblock fields.
*/

View File

@@ -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),

View File

@@ -0,0 +1 @@
@import "./status-widget/StatusWidget";

View File

@@ -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;
}
}

View File

@@ -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 }) => (
<b>
<FormattedDate
value={date}
year="numeric"
month="long"
day="2-digit"
/>
</b>
);
const CustomFormattedTime = ({ date }: { date: string }) => (
<b>
<FormattedTime
value={date}
hour12={false}
/>
</b>
);
type DraftBodyMessageProps = {
lastDraftCreatedBy: string | null;
lastDraftCreated: string | null;
created: string | null;
};
const DraftBodyMessage = ({ lastDraftCreatedBy, lastDraftCreated, created }: DraftBodyMessageProps) => {
if (lastDraftCreatedBy && lastDraftCreated) {
return (
<FormattedMessage
{...messages.lastDraftMsg}
values={{
date: <CustomFormattedDate date={lastDraftCreated} />,
time: <CustomFormattedTime date={lastDraftCreated} />,
user: <b>{lastDraftCreatedBy}</b>,
}}
/>
);
}
if (lastDraftCreated) {
return (
<FormattedMessage
{...messages.lastDraftMsgWithoutUser}
values={{
date: <CustomFormattedDate date={lastDraftCreated} />,
time: <CustomFormattedTime date={lastDraftCreated} />,
}}
/>
);
}
if (created) {
return (
<FormattedMessage
{...messages.lastDraftMsgWithoutUser}
values={{
date: <CustomFormattedDate date={created} />,
time: <CustomFormattedTime date={created} />,
}}
/>
);
}
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 <StatusWidget {...libraryData} onCommit={onCommit} onRevert={onRevert} />;
* ```
*/
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 = (<DraftBodyMessage {...{ lastDraftCreatedBy, lastDraftCreated, created }} />);
} else if (hasUnpublishedChanges || hasUnpublishedDeletes) {
// Entity is on Draft state
isPublished = false;
statusMessage = intl.formatMessage(messages.draftStatusLabel);
extraStatusMessage = intl.formatMessage(messages.unpublishedStatusLabel);
bodyMessage = (<DraftBodyMessage {...{ lastDraftCreatedBy, lastDraftCreated, created }} />);
} else {
// Entity is published
isPublished = true;
statusMessage = intl.formatMessage(messages.publishedStatusLabel);
if (publishedBy) {
bodyMessage = (
<FormattedMessage
{...messages.lastPublishedMsg}
values={{
date: <CustomFormattedDate date={lastPublished} />,
time: <CustomFormattedTime date={lastPublished} />,
user: <b>{publishedBy}</b>,
}}
/>
);
} else {
bodyMessage = (
<FormattedMessage
{...messages.lastPublishedMsgWithoutUser}
values={{
date: <CustomFormattedDate date={lastPublished} />,
time: <CustomFormattedTime date={lastPublished} />,
}}
/>
);
}
}
return (
<Stack>
<Container className={classNames('status-widget', {
'draft-status': !isPublished,
'published-status': isPublished,
})}
>
<span className="font-weight-bold">
{statusMessage}
</span>
{ extraStatusMessage && (
<span className="ml-1">
{extraStatusMessage}
</span>
)}
</Container>
<Container className="mt-3">
<Stack gap={3}>
<span>
{bodyMessage}
</span>
{onCommit && (
<Button disabled={isPublished} onClick={onCommit}>
{intl.formatMessage(messages.publishButtonLabel)}
</Button>
)}
{onRevert && (
<div className="d-flex justify-content-end">
<Button disabled={isPublished || isNew} variant="link" onClick={onRevert}>
{intl.formatMessage(messages.discardChangesButtonLabel)}
</Button>
</div>
)}
</Stack>
</Container>
</Stack>
);
};
export default StatusWidget;

View File

@@ -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;

View File

@@ -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";

View File

@@ -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) => (
<b>
<FormattedDate
value={date}
year="numeric"
month="long"
day="2-digit"
/>
</b>
));
const buildTime = ((date: string) => (
<b>
<FormattedTime
value={date}
hour12={false}
/>
</b>
));
const buildDraftBodyMessage = (() => {
if (library.lastDraftCreatedBy && library.lastDraftCreated) {
return intl.formatMessage(messages.lastDraftMsg, {
date: buildDate(library.lastDraftCreated),
time: buildTime(library.lastDraftCreated),
user: <b>{library.lastDraftCreatedBy}</b>,
});
}
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: <b>{library.publishedBy}</b>,
});
} 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 (
<Stack>
<Container className={classNames('library-publish-status', {
'draft-status': !isPublished,
'published-status': isPublished,
})}
>
<span className="font-weight-bold">
{statusMessage}
</span>
{ extraStatusMessage && (
<span className="ml-1">
{extraStatusMessage}
</span>
)}
</Container>
<Container className="mt-3">
<Stack gap={3}>
<span>
{bodyMessage}
</span>
<Button disabled={isPublished} onClick={commit}>
{intl.formatMessage(messages.publishButtonLabel)}
</Button>
<div className="d-flex justify-content-end">
<Button disabled={isPublished || isNew} variant="link" onClick={revert}>
{intl.formatMessage(messages.discardChangesButtonLabel)}
</Button>
</div>
</Stack>
</Container>
</Stack>
<StatusWidget
{...library}
onCommit={commit}
onRevert={revert}
/>
);
};

View File

@@ -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',

View File

@@ -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();