Skip to content

fix: use Library search results to populate container card preview [FC-0083] #1820

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions src/library-authoring/components/ContainerCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
fireEvent,
} from '../../testUtils';
import { LibraryProvider } from '../common/context/LibraryContext';
import { mockContentLibrary, mockGetContainerChildren } from '../data/api.mocks';
import { mockContentLibrary } from '../data/api.mocks';
import { type ContainerHit, PublishStatus } from '../../search-manager';
import ContainerCard from './ContainerCard';
import { getLibraryContainerApiUrl, getLibraryContainerRestoreApiUrl } from '../data/api';
Expand Down Expand Up @@ -40,7 +40,6 @@ let axiosMock: MockAdapter;
let mockShowToast;

mockContentLibrary.applyMock();
mockGetContainerChildren.applyMock();

const render = (ui: React.ReactElement, showOnlyPublished: boolean = false) => baseRender(ui, {
extraWrapper: ({ children }) => (
Expand Down Expand Up @@ -155,29 +154,54 @@ describe('<ContainerCard />', () => {
it('should render no child blocks in card preview', async () => {
render(<ContainerCard hit={containerHitSample} />);

expect(screen.queryByTitle('text block')).not.toBeInTheDocument();
expect(screen.queryByTitle('lb:org1:Demo_course:html:text-0')).not.toBeInTheDocument();
expect(screen.queryByText('+0')).not.toBeInTheDocument();
});

it('should render <=5 child blocks in card preview', async () => {
const containerWith5Children = {
...containerHitSample,
usageKey: mockGetContainerChildren.fiveChildren,
};
content: {
childUsageKeys: Array(5).fill('').map((_child, idx) => `lb:org1:Demo_course:html:text-${idx}`),
},
} satisfies ContainerHit;
render(<ContainerCard hit={containerWith5Children} />);

expect((await screen.findAllByTitle(/text block */)).length).toBe(5);
expect((await screen.findAllByTitle(/lb:org1:Demo_course:html:text-*/)).length).toBe(5);
expect(screen.queryByText('+0')).not.toBeInTheDocument();
});

it('should render >5 child blocks with +N in card preview', async () => {
const containerWith6Children = {
...containerHitSample,
usageKey: mockGetContainerChildren.sixChildren,
};
content: {
childUsageKeys: Array(6).fill('').map((_child, idx) => `lb:org1:Demo_course:html:text-${idx}`),
},
} satisfies ContainerHit;
render(<ContainerCard hit={containerWith6Children} />);

expect((await screen.findAllByTitle(/text block */)).length).toBe(4);
expect((await screen.findAllByTitle(/lb:org1:Demo_course:html:text-*/)).length).toBe(4);
expect(screen.queryByText('+2')).toBeInTheDocument();
});

it('should render published child blocks when rendering a published card preview', async () => {
const containerWithPublishedChildren = {
...containerHitSample,
content: {
childUsageKeys: Array(6).fill('').map((_child, idx) => `lb:org1:Demo_course:html:text-${idx}`),
},
published: {
content: {
childUsageKeys: Array(2).fill('').map((_child, idx) => `lb:org1:Demo_course:html:text-${idx}`),
},
},
} satisfies ContainerHit;
render(
<ContainerCard hit={containerWithPublishedChildren} />,
true,
);

expect((await screen.findAllByTitle(/lb:org1:Demo_course:html:text-*/)).length).toBe(2);
expect(screen.queryByText('+2')).not.toBeInTheDocument();
});
});
28 changes: 15 additions & 13 deletions src/library-authoring/components/ContainerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import { MoreVert } from '@openedx/paragon/icons';
import { Link } from 'react-router-dom';

import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils';
import { getBlockType } from '../../generic/key-utils';
import { ToastContext } from '../../generic/toast-context';
import { type ContainerHit, PublishStatus } from '../../search-manager';
import { useComponentPickerContext } from '../common/context/ComponentPickerContext';
import { useLibraryContext } from '../common/context/LibraryContext';
import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext';
import { useContainerChildren, useRemoveItemsFromCollection } from '../data/apiHooks';
import { useRemoveItemsFromCollection } from '../data/apiHooks';
import { useLibraryRoutes } from '../routes';
import AddComponentWidget from './AddComponentWidget';
import BaseCard from './BaseCard';
Expand Down Expand Up @@ -107,21 +108,17 @@ const ContainerMenu = ({ hit } : ContainerMenuProps) => {
};

type ContainerCardPreviewProps = {
containerId: string;
childUsageKeys: Array<string>;
showMaxChildren?: number;
};

const ContainerCardPreview = ({ containerId, showMaxChildren = 5 }: ContainerCardPreviewProps) => {
const { data, isLoading, isError } = useContainerChildren(containerId);
if (isLoading || isError) {
return null;
}

const hiddenChildren = data.length - showMaxChildren;
const ContainerCardPreview = ({ childUsageKeys, showMaxChildren = 5 }: ContainerCardPreviewProps) => {
const hiddenChildren = childUsageKeys.length - showMaxChildren;
return (
<Stack direction="horizontal" gap={2}>
{
data.slice(0, showMaxChildren).map(({ id, blockType, displayName }, idx) => {
childUsageKeys.slice(0, showMaxChildren).map((usageKey, idx) => {
const blockType = getBlockType(usageKey);
let blockPreview: ReactNode;
let classNames;

Expand All @@ -133,7 +130,7 @@ const ContainerCardPreview = ({ containerId, showMaxChildren = 5 }: ContainerCar
<Icon
src={getItemIcon(blockType)}
screenReaderText={blockType}
title={displayName}
title={usageKey}
/>
);
} else {
Expand All @@ -149,7 +146,7 @@ const ContainerCardPreview = ({ containerId, showMaxChildren = 5 }: ContainerCar
<div
// A container can have multiple instances of the same block
// eslint-disable-next-line react/no-array-index-key
key={`${id}-${idx}`}
key={`${usageKey}-${idx}`}
className={classNames}
>
{blockPreview}
Expand Down Expand Up @@ -178,6 +175,7 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
published,
publishStatus,
usageKey: unitId,
content,
} = hit;

const numChildrenCount = showOnlyPublished ? (
Expand All @@ -188,6 +186,10 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
showOnlyPublished ? formatted.published?.displayName : formatted.displayName
) ?? '';

const childUsageKeys: Array<string> = (
showOnlyPublished ? published?.content?.childUsageKeys : content?.childUsageKeys
) ?? [];

const { navigateTo } = useLibraryRoutes();

const openContainer = useCallback(() => {
Expand All @@ -202,7 +204,7 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
<BaseCard
itemType={itemType}
displayName={displayName}
preview={<ContainerCardPreview containerId={unitId} />}
preview={<ContainerCardPreview childUsageKeys={childUsageKeys} />}
tags={tags}
numChildren={numChildrenCount}
actions={(
Expand Down
12 changes: 9 additions & 3 deletions src/search-manager/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const getContentSearchConfig = async (): Promise<{ url: string, indexName
export interface ContentDetails {
htmlContent?: string;
capaContent?: string;
childUsageKeys?: Array<string>;
[k: string]: any;
}

Expand Down Expand Up @@ -151,9 +152,10 @@ export interface ContentHit extends BaseContentHit {
* Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py
*/
export interface ContentPublishedData {
description?: string,
displayName?: string,
numChildren?: number,
description?: string;
displayName?: string;
numChildren?: number;
content?: ContentDetails;
}

/**
Expand All @@ -171,13 +173,17 @@ export interface CollectionHit extends BaseContentHit {
* Information about a single container returned in the search results
* Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py
*/
interface ContainerHitContent {
childUsageKeys?: string[],
}
export interface ContainerHit extends BaseContentHit {
type: 'library_container';
blockType: 'unit'; // This should be expanded to include other container types
numChildren?: number;
published?: ContentPublishedData;
publishStatus: PublishStatus;
formatted: BaseContentHit['formatted'] & { published?: ContentPublishedData, };
content?: ContainerHitContent;
}

export type HitType = ContentHit | CollectionHit | ContainerHit;
Expand Down