Skip to content

Commit

Permalink
feat: add component Details sidebar [FC-0062] (#1303)
Browse files Browse the repository at this point in the history
* feat: add ComponentDetails component

---------

Co-authored-by: Jillian <[email protected]>
  • Loading branch information
rpenido and pomegranited authored Sep 25, 2024
1 parent c13ab00 commit ff67c9a
Show file tree
Hide file tree
Showing 14 changed files with 203 additions and 8 deletions.
42 changes: 42 additions & 0 deletions src/library-authoring/component-info/ComponentDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
initializeMocks,
render,
screen,
} from '../../testUtils';
import { mockLibraryBlockMetadata } from '../data/api.mocks';
import ComponentDetails from './ComponentDetails';

describe('<ComponentDetails />', () => {
it('should render the component details loading', async () => {
initializeMocks();
mockLibraryBlockMetadata.applyMock();
render(<ComponentDetails usageKey={mockLibraryBlockMetadata.usageKeyThatNeverLoads} />);
expect(await screen.findByText('Loading...')).toBeInTheDocument();
});

it('should render the component details error', async () => {
initializeMocks();
mockLibraryBlockMetadata.applyMock();
render(<ComponentDetails usageKey={mockLibraryBlockMetadata.usageKeyError404} />);
expect(await screen.findByText(/Mocked request failed with status code 404/)).toBeInTheDocument();
});

it('should render the component usage', async () => {
initializeMocks();
mockLibraryBlockMetadata.applyMock();
render(<ComponentDetails usageKey={mockLibraryBlockMetadata.usageKeyNeverPublished} />);
expect(await screen.findByText('Component Usage')).toBeInTheDocument();
// TODO: replace with actual data when implement tag list
expect(screen.queryByText('This will show the courses that use this component.')).toBeInTheDocument();
});

it('should render the component history', async () => {
initializeMocks();
mockLibraryBlockMetadata.applyMock();
render(<ComponentDetails usageKey={mockLibraryBlockMetadata.usageKeyNeverPublished} />);
// Show created date
expect(await screen.findByText('June 20, 2024')).toBeInTheDocument();
// Show modified date
expect(await screen.findByText('June 21, 2024')).toBeInTheDocument();
});
});
57 changes: 57 additions & 0 deletions src/library-authoring/component-info/ComponentDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { Stack } from '@openedx/paragon';

import AlertError from '../../generic/alert-error';
import Loading from '../../generic/Loading';
import { useLibraryBlockMetadata } from '../data/apiHooks';
import HistoryWidget from '../generic/history-widget';
import { ComponentDeveloperInfo } from './ComponentDeveloperInfo';
import messages from './messages';

interface ComponentDetailsProps {
usageKey: string;
}

const ComponentDetails = ({ usageKey }: ComponentDetailsProps) => {
const intl = useIntl();
const {
data: componentMetadata,
isError,
error,
isLoading,
} = useLibraryBlockMetadata(usageKey);

if (isError) {
return <AlertError error={error} />;
}

if (isLoading) {
return <Loading />;
}

return (
<Stack gap={3}>
<div>
<h3 className="h5">
{intl.formatMessage(messages.detailsTabUsageTitle)}
</h3>
<small>This will show the courses that use this component.</small>
</div>
<hr className="w-100" />
<div>
<h3 className="h5">
{intl.formatMessage(messages.detailsTabHistoryTitle)}
</h3>
<HistoryWidget
{...componentMetadata}
/>
</div>
{
// istanbul ignore next: this is only shown in development
(process.env.NODE_ENV === 'development' ? <ComponentDeveloperInfo usageKey={usageKey} /> : null)
}
</Stack>
);
};

export default ComponentDetails;
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const ComponentDeveloperInfo: React.FC<Props> = ({ usageKey }) => {
const { data: olx, isLoading: isOLXLoading } = useXBlockOLX(usageKey);
return (
<>
<hr />
<hr className="w-100" />
<h3 className="h5">Developer Component Details</h3>
<p><small>(This panel is only visible in development builds.)</small></p>
<dl>
Expand Down
8 changes: 2 additions & 6 deletions src/library-authoring/component-info/ComponentInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Link } from 'react-router-dom';

import { getEditUrl } from '../components/utils';
import { ComponentMenu } from '../components';
import { ComponentDeveloperInfo } from './ComponentDeveloperInfo';
import ComponentDetails from './ComponentDetails';
import ComponentManagement from './ComponentManagement';
import ComponentPreview from './ComponentPreview';
import messages from './messages';
Expand Down Expand Up @@ -50,11 +50,7 @@ const ComponentInfo = ({ usageKey }: ComponentInfoProps) => {
<ComponentManagement usageKey={usageKey} />
</Tab>
<Tab eventKey="details" title={intl.formatMessage(messages.detailsTabTitle)}>
Details tab placeholder

{
(process.env.NODE_ENV === 'development' ? <ComponentDeveloperInfo usageKey={usageKey} /> : null)
}
<ComponentDetails usageKey={usageKey} />
</Tab>
</Tabs>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { mockLibraryBlockMetadata } from '../data/api.mocks';
import ComponentManagement from './ComponentManagement';

/*
* FIXME: Summarize the reason here
* This function is used to get the inner text of an element.
* https://stackoverflow.com/questions/47902335/innertext-is-undefined-in-jest-test
*/
const getInnerText = (element: Element) => element?.textContent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const ComponentManagement = ({ usageKey }: ComponentManagementProps) => {
const intl = useIntl();
const { data: componentMetadata } = useLibraryBlockMetadata(usageKey);

// istanbul ignore if: this should never happen
if (!componentMetadata) {
return null;
}
Expand Down
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 @@ -51,6 +51,16 @@ const messages = defineMessages({
defaultMessage: 'Details',
description: 'Title for details tab',
},
detailsTabUsageTitle: {
id: 'course-authoring.library-authoring.component.details-tab.usage-title',
defaultMessage: 'Component Usage',
description: 'Title for the Component Usage container in the details tab',
},
detailsTabHistoryTitle: {
id: 'course-authoring.library-authoring.component.details-tab.history-title',
defaultMessage: 'Component History',
description: 'Title for the Component History container in the details tab',
},
previewExpandButtonTitle: {
id: 'course-authoring.library-authoring.component.preview.expand.title',
defaultMessage: 'Expand',
Expand Down
12 changes: 12 additions & 0 deletions src/library-authoring/data/api.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ mockCreateLibraryBlock.newHtmlData = {
lastDraftCreated: '2024-07-22T21:37:49Z',
lastDraftCreatedBy: null,
created: '2024-07-22T21:37:49Z',
modified: '2024-07-22T21:37:49Z',
tagsCount: 0,
} satisfies api.LibraryBlockMetadata;
mockCreateLibraryBlock.newProblemData = {
Expand All @@ -147,6 +148,7 @@ mockCreateLibraryBlock.newProblemData = {
lastDraftCreated: '2024-07-22T21:37:49Z',
lastDraftCreatedBy: null,
created: '2024-07-22T21:37:49Z',
modified: '2024-07-22T21:37:49Z',
tagsCount: 0,
} satisfies api.LibraryBlockMetadata;
mockCreateLibraryBlock.newVideoData = {
Expand All @@ -160,6 +162,7 @@ mockCreateLibraryBlock.newVideoData = {
lastDraftCreated: '2024-07-22T21:37:49Z',
lastDraftCreatedBy: null,
created: '2024-07-22T21:37:49Z',
modified: '2024-07-22T21:37:49Z',
tagsCount: 0,
} satisfies api.LibraryBlockMetadata;
/** Apply this mock. Returns a spy object that can tell you if it's been called. */
Expand Down Expand Up @@ -224,11 +227,18 @@ mockXBlockFields.applyMock = () => jest.spyOn(api, 'getXBlockFields').mockImplem
export async function mockLibraryBlockMetadata(usageKey: string): Promise<api.LibraryBlockMetadata> {
const thisMock = mockLibraryBlockMetadata;
switch (usageKey) {
case thisMock.usageKeyThatNeverLoads:
// Return a promise that never resolves, to simulate never loading:
return new Promise<any>(() => {});
case thisMock.usageKeyError404:
throw createAxiosError({ code: 404, message: 'Not found.', path: api.getLibraryBlockMetadataUrl(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.usageKeyThatNeverLoads = 'lb:Axim:infiniteLoading:html:123';
mockLibraryBlockMetadata.usageKeyError404 = 'lb:Axim:error404:html:123';
mockLibraryBlockMetadata.usageKeyNeverPublished = 'lb:Axim:TEST1:html:571fe018-f3ce-45c9-8f53-5dafcb422fd1';
mockLibraryBlockMetadata.dataNeverPublished = {
id: 'lb:Axim:TEST1:html:571fe018-f3ce-45c9-8f53-5dafcb422fd1',
Expand All @@ -241,6 +251,7 @@ mockLibraryBlockMetadata.dataNeverPublished = {
lastDraftCreatedBy: null,
hasUnpublishedChanges: false,
created: '2024-06-20T13:54:21Z',
modified: '2024-06-21T13:54:21Z',
tagsCount: 0,
} satisfies api.LibraryBlockMetadata;
mockLibraryBlockMetadata.usageKeyPublished = 'lb:Axim:TEST2:html:571fe018-f3ce-45c9-8f53-5dafcb422fd2';
Expand All @@ -255,6 +266,7 @@ mockLibraryBlockMetadata.dataPublished = {
lastDraftCreatedBy: '2024-06-20T20:00:00Z',
hasUnpublishedChanges: false,
created: '2024-06-20T13:54:21Z',
modified: '2024-06-21T13:54:21Z',
tagsCount: 0,
} satisfies api.LibraryBlockMetadata;
/** Apply this mock. Returns a spy object that can tell you if it's been called. */
Expand Down
1 change: 1 addition & 0 deletions src/library-authoring/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export interface LibraryBlockMetadata {
lastDraftCreatedBy: string | null,
hasUnpublishedChanges: boolean;
created: string | null,
modified: string | null,
tagsCount: number;
}

Expand Down
1 change: 1 addition & 0 deletions src/library-authoring/data/apiHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const xblockQueryKeys = {
*/
export function invalidateComponentData(queryClient: QueryClient, contentLibraryId: string, usageKey: string) {
queryClient.invalidateQueries({ queryKey: xblockQueryKeys.xblockFields(usageKey) });
queryClient.invalidateQueries({ queryKey: xblockQueryKeys.componentMetadata(usageKey) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, contentLibraryId) });
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.history-widget-bar {
border-left: 8px solid $info-300;
border-radius: 4px;
padding-left: 1rem;
}

52 changes: 52 additions & 0 deletions src/library-authoring/generic/history-widget/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
import { Stack } from '@openedx/paragon';

import messages from './messages';

const CustomFormattedDate = ({ date }: { date: string }) => (
<FormattedDate
value={date}
year="numeric"
month="long"
day="2-digit"
/>
);

type HistoryWidgedProps = {
modified: string | null;
created: string | null;
};

/**
* This component displays the history of an entity (Last Modified and Created dates)
*
* This component doesn't handle fetching the data or any other side effects. It only displays the dates.
*
* @example
* ```tsx
* const { data: componentMetadata } = useLibraryBlockMetadata(usageKey);
*
* return <HistoryWidget {...componentMetadata} />;
* ```
*/
const HistoryWidget = ({
modified,
created,
}: HistoryWidgedProps) => (
<Stack className="history-widget-bar small" gap={3}>
{modified && (
<div>
<div className="text-muted"><FormattedMessage {...messages.lastModifiedTitle} /> </div>
<CustomFormattedDate date={modified} />
</div>
)}
{created && (
<div>
<div className="text-muted"><FormattedMessage {...messages.createdTitle} /> </div>
<CustomFormattedDate date={created} />
</div>
)}
</Stack>
);

export default HistoryWidget;
16 changes: 16 additions & 0 deletions src/library-authoring/generic/history-widget/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineMessages } from '@edx/frontend-platform/i18n';

const messages = defineMessages({
lastModifiedTitle: {
id: 'course-authoring.library-authoring.generic.history-widget.last-modified',
defaultMessage: 'Last Modified',
description: 'Title of the last modified section in the library authoring sidebar.',
},
createdTitle: {
id: 'course-authoring.library-authoring.generic.history-widget.created',
defaultMessage: 'Created',
description: 'Title of the created section in the library authoring sidebar.',
},
});

export default messages;
1 change: 1 addition & 0 deletions src/library-authoring/generic/index.scss
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
@import "./status-widget/StatusWidget";
@import "./history-widget/HistoryWidget";

0 comments on commit ff67c9a

Please sign in to comment.