Skip to content

Commit

Permalink
feat: add component sidebar manage tab [FC-0062] (#1275)
Browse files Browse the repository at this point in the history
  • Loading branch information
rpenido authored Sep 16, 2024
1 parent 902853d commit dd7e4d4
Show file tree
Hide file tree
Showing 17 changed files with 512 additions and 196 deletions.
3 changes: 2 additions & 1 deletion src/library-authoring/component-info/ComponentInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
70 changes: 70 additions & 0 deletions src/library-authoring/component-info/ComponentManagement.test.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
});
57 changes: 57 additions & 0 deletions src/library-authoring/component-info/ComponentManagement.tsx
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 10 additions & 0 deletions src/library-authoring/component-info/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
60 changes: 58 additions & 2 deletions src/library-authoring/data/api.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);
27 changes: 24 additions & 3 deletions src/library-authoring/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/`;

/**
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand All @@ -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.
*/
Expand Down
9 changes: 9 additions & 0 deletions src/library-authoring/data/apiHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
revertLibraryChanges,
updateLibraryMetadata,
libraryPasteClipboard,
getLibraryBlockMetadata,
getXBlockFields,
updateXBlockFields,
createCollection,
Expand Down Expand Up @@ -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'],
};

/**
Expand Down Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions src/library-authoring/generic/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "./status-widget/StatusWidget";
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.library-publish-status {
.status-widget {
&.draft-status {
background-color: #FDF3E9;
border-top: 4px solid #F4B57B;
Expand All @@ -9,3 +9,4 @@
border-top: 4px solid $info-400;
}
}

Loading

0 comments on commit dd7e4d4

Please sign in to comment.