feat: Section/Subsection Card Preview [FC-0090] (#2057)
Section/Subsection card previews
This commit is contained in:
7
src/library-authoring/components/ContainerCard.scss
Normal file
7
src/library-authoring/components/ContainerCard.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.container-card-preview-text {
|
||||
display: -webkit-box; /* stylelint-disable-line value-no-vendor-prefix */
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
};
|
||||
@@ -68,17 +68,51 @@ describe('<ContainerCard />', () => {
|
||||
({ axiosMock, mockShowToast } = initializeMocks());
|
||||
});
|
||||
|
||||
it('should render the card with title', () => {
|
||||
render(<ContainerCard hit={getContainerHitSample()} />);
|
||||
test.each([
|
||||
{
|
||||
label: 'should render the unit card with title',
|
||||
containerType: ContainerType.Unit,
|
||||
displayName: 'unit Display Formated Name',
|
||||
},
|
||||
{
|
||||
label: 'should render the subsection card with title',
|
||||
containerType: ContainerType.Subsection,
|
||||
displayName: 'subsection Display Formated Name',
|
||||
},
|
||||
{
|
||||
label: 'should render the section card with title',
|
||||
containerType: ContainerType.Section,
|
||||
displayName: 'section Display Formated Name',
|
||||
},
|
||||
])('$label', ({ containerType, displayName }) => {
|
||||
const container = getContainerHitSample(containerType);
|
||||
render(<ContainerCard hit={container} />);
|
||||
|
||||
expect(screen.getByText('unit Display Formated Name')).toBeInTheDocument();
|
||||
expect(screen.getByText(displayName)).toBeInTheDocument();
|
||||
expect(screen.queryByText('2')).toBeInTheDocument(); // Component count
|
||||
});
|
||||
|
||||
it('should render published content', () => {
|
||||
render(<ContainerCard hit={getContainerHitSample()} />, true);
|
||||
test.each([
|
||||
{
|
||||
label: 'sould render published content of unit card',
|
||||
containerType: ContainerType.Unit,
|
||||
displayName: 'Published unit Display Name',
|
||||
},
|
||||
{
|
||||
label: 'sould render published content of subsection card',
|
||||
containerType: ContainerType.Subsection,
|
||||
displayName: 'Published subsection Display Name',
|
||||
},
|
||||
{
|
||||
label: 'sould render published content of section card',
|
||||
containerType: ContainerType.Section,
|
||||
displayName: 'Published section Display Name',
|
||||
},
|
||||
])('$label', ({ containerType, displayName }) => {
|
||||
const container = getContainerHitSample(containerType);
|
||||
render(<ContainerCard hit={container} />, true);
|
||||
|
||||
expect(screen.getByText('Published unit Display Name')).toBeInTheDocument();
|
||||
expect(screen.getByText(displayName)).toBeInTheDocument();
|
||||
expect(screen.queryByText('1')).toBeInTheDocument(); // Published Component Count
|
||||
});
|
||||
|
||||
@@ -153,7 +187,7 @@ describe('<ContainerCard />', () => {
|
||||
fireEvent.click(deleteMenuItem);
|
||||
|
||||
// Confirm delete Modal is open
|
||||
expect(screen.getByText('Delete Unit'));
|
||||
expect(await screen.findByText('Delete Unit')).toBeInTheDocument();
|
||||
const deleteButton = screen.getByRole('button', { name: /delete/i });
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
@@ -200,14 +234,14 @@ describe('<ContainerCard />', () => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Failed to delete unit');
|
||||
});
|
||||
|
||||
it('should render no child blocks in card preview', async () => {
|
||||
it('should render no child blocks in unit card preview', async () => {
|
||||
render(<ContainerCard hit={getContainerHitSample()} />);
|
||||
|
||||
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 () => {
|
||||
it('should render <=5 child blocks in unit card preview', async () => {
|
||||
const containerWith5Children = {
|
||||
...getContainerHitSample(),
|
||||
content: {
|
||||
@@ -220,7 +254,7 @@ describe('<ContainerCard />', () => {
|
||||
expect(screen.queryByText('+0')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render >5 child blocks with +N in card preview', async () => {
|
||||
it('should render >5 child blocks with +N in unit card preview', async () => {
|
||||
const containerWith6Children = {
|
||||
...getContainerHitSample(),
|
||||
content: {
|
||||
@@ -233,7 +267,7 @@ describe('<ContainerCard />', () => {
|
||||
expect(screen.queryByText('+2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render published child blocks when rendering a published card preview', async () => {
|
||||
it('should render published child blocks when rendering a published unit card preview', async () => {
|
||||
const containerWithPublishedChildren = {
|
||||
...getContainerHitSample(),
|
||||
content: {
|
||||
@@ -253,4 +287,104 @@ describe('<ContainerCard />', () => {
|
||||
expect((await screen.findAllByTitle(/lb:org1:Demo_course:html:text-*/)).length).toBe(2);
|
||||
expect(screen.queryByText('+2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
label: 'should render published child in subsection card preview',
|
||||
containerType: ContainerType.Subsection,
|
||||
childrenType: 'unit',
|
||||
displayName: 'Published subsection Display Name',
|
||||
expected: /contains unit 0, unit 1\./i,
|
||||
},
|
||||
{
|
||||
label: 'should render published child in section card preview',
|
||||
containerType: ContainerType.Section,
|
||||
childrenType: 'subsection',
|
||||
displayName: 'Published section Display Name',
|
||||
expected: /contains subsection 0, subsection 1\./i,
|
||||
},
|
||||
])('$label', ({
|
||||
containerType,
|
||||
childrenType,
|
||||
displayName,
|
||||
expected,
|
||||
}) => {
|
||||
const containerWithChildren = {
|
||||
...getContainerHitSample(containerType),
|
||||
content: {
|
||||
childUsageKeys: Array(6).fill('').map(
|
||||
(_child, idx) => `lct:org1:Demo_Course:${childrenType}:${childrenType}-${idx}`,
|
||||
),
|
||||
childDisplayNames: Array(6).fill('').map((_child, idx) => `${childrenType} ${idx}`),
|
||||
},
|
||||
published: {
|
||||
content: {
|
||||
childUsageKeys: Array(2).fill('').map(
|
||||
(_child, idx) => `lct:org1:Demo_Course:${childrenType}:${childrenType}-${idx}`,
|
||||
),
|
||||
childDisplayNames: Array(2).fill('').map((_child, idx) => `${childrenType} ${idx}`),
|
||||
},
|
||||
},
|
||||
} satisfies ContainerHit;
|
||||
|
||||
render(<ContainerCard hit={containerWithChildren} />, true);
|
||||
|
||||
expect(screen.getByText(displayName)).toBeInTheDocument();
|
||||
expect(screen.getByText(expected)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
label: 'should render subsection card preview with children',
|
||||
containerType: ContainerType.Subsection,
|
||||
childrenType: 'unit',
|
||||
displayName: 'subsection Display Formated Name',
|
||||
expected: /contains unit 0, unit 1\./i,
|
||||
},
|
||||
{
|
||||
label: 'should render section card preview with children',
|
||||
containerType: ContainerType.Section,
|
||||
childrenType: 'subsection',
|
||||
displayName: 'section Display Formated Name',
|
||||
expected: /contains subsection 0, subsection 1\./i,
|
||||
},
|
||||
])('$label', ({
|
||||
containerType,
|
||||
childrenType,
|
||||
displayName,
|
||||
expected,
|
||||
}) => {
|
||||
const containerWithChildren = {
|
||||
...getContainerHitSample(containerType),
|
||||
content: {
|
||||
childUsageKeys: Array(2).fill('').map(
|
||||
(_child, idx) => `lct:org1:Demo_Course:${childrenType}:${childrenType}-${idx}`,
|
||||
),
|
||||
childDisplayNames: Array(2).fill('').map((_child, idx) => `${childrenType} ${idx}`),
|
||||
},
|
||||
} satisfies ContainerHit;
|
||||
render(<ContainerCard hit={containerWithChildren} />);
|
||||
|
||||
expect(screen.getByText(displayName)).toBeInTheDocument();
|
||||
expect(screen.getByText(expected)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
label: 'should render subsection card preview without children',
|
||||
containerType: ContainerType.Subsection,
|
||||
displayName: 'subsection Display Formated Name',
|
||||
},
|
||||
{
|
||||
label: 'should render section card preview without children',
|
||||
containerType: ContainerType.Section,
|
||||
displayName: 'section Display Formated Name',
|
||||
},
|
||||
])('$label', ({ containerType, displayName }) => {
|
||||
const container = getContainerHitSample(containerType);
|
||||
render(<ContainerCard hit={container} />);
|
||||
|
||||
expect(screen.getByText(displayName)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/contains/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import { MoreVert } from '@openedx/paragon/icons';
|
||||
import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils';
|
||||
import { ContainerType, getBlockType } from '../../generic/key-utils';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
import { type ContainerHit, PublishStatus } from '../../search-manager';
|
||||
import { type ContainerHit, Highlight, PublishStatus } from '../../search-manager';
|
||||
import { useComponentPickerContext } from '../common/context/ComponentPickerContext';
|
||||
import { useLibraryContext } from '../common/context/LibraryContext';
|
||||
import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext';
|
||||
@@ -88,7 +88,7 @@ export const ContainerMenu = ({ containerKey, containerType, displayName } : Con
|
||||
<Dropdown.Item onClick={openContainer}>
|
||||
<FormattedMessage {...messages.menuOpen} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onClick={confirmDelete}>
|
||||
<Dropdown.Item onClick={confirmDelete} disabled={containerType !== 'unit'}>
|
||||
<FormattedMessage {...messages.menuDeleteContainer} />
|
||||
</Dropdown.Item>
|
||||
{insideCollection && (
|
||||
@@ -96,7 +96,7 @@ export const ContainerMenu = ({ containerKey, containerType, displayName } : Con
|
||||
<FormattedMessage {...messages.menuRemoveFromCollection} />
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item onClick={showManageCollections}>
|
||||
<Dropdown.Item onClick={showManageCollections} disabled={containerType !== 'unit'}>
|
||||
<FormattedMessage {...messages.menuAddToCollection} />
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
@@ -111,17 +111,17 @@ export const ContainerMenu = ({ containerKey, containerType, displayName } : Con
|
||||
);
|
||||
};
|
||||
|
||||
type ContainerCardPreviewProps = {
|
||||
childUsageKeys: Array<string>;
|
||||
type UnitCardPreviewProps = {
|
||||
childKeys: Array<string>;
|
||||
showMaxChildren?: number;
|
||||
};
|
||||
|
||||
const ContainerCardPreview = ({ childUsageKeys, showMaxChildren = 5 }: ContainerCardPreviewProps) => {
|
||||
const hiddenChildren = childUsageKeys.length - showMaxChildren;
|
||||
const UnitcardPreview = ({ childKeys, showMaxChildren = 5 }: UnitCardPreviewProps) => {
|
||||
const hiddenChildren = childKeys.length - showMaxChildren;
|
||||
return (
|
||||
<Stack direction="horizontal" gap={2}>
|
||||
{
|
||||
childUsageKeys.slice(0, showMaxChildren).map((usageKey, idx) => {
|
||||
childKeys.slice(0, showMaxChildren).map((usageKey, idx) => {
|
||||
const blockType = getBlockType(usageKey);
|
||||
let blockPreview: ReactNode;
|
||||
let classNames;
|
||||
@@ -162,6 +162,51 @@ const ContainerCardPreview = ({ childUsageKeys, showMaxChildren = 5 }: Container
|
||||
);
|
||||
};
|
||||
|
||||
type ContainerCardPreviewProps = {
|
||||
hit: ContainerHit,
|
||||
};
|
||||
|
||||
const ContainerCardPreview = ({ hit }: ContainerCardPreviewProps) => {
|
||||
const intl = useIntl();
|
||||
const { showOnlyPublished } = useLibraryContext();
|
||||
const {
|
||||
blockType: itemType,
|
||||
published,
|
||||
content,
|
||||
} = hit;
|
||||
|
||||
if (itemType === 'unit') {
|
||||
const childKeys: Array<string> = (
|
||||
showOnlyPublished ? published?.content?.childUsageKeys : content?.childUsageKeys
|
||||
) ?? [];
|
||||
|
||||
return <UnitcardPreview childKeys={childKeys} />;
|
||||
}
|
||||
// TODO Section highlights
|
||||
|
||||
const childNames: Array<string> = (
|
||||
showOnlyPublished ? published?.content?.childDisplayNames : content?.childDisplayNames
|
||||
) ?? [];
|
||||
|
||||
if (childNames.length > 0) {
|
||||
// Preview with a truncated text with all children display names
|
||||
const childrenText = intl.formatMessage(
|
||||
messages.containerPreviewText,
|
||||
{
|
||||
children: childNames.join(', '),
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container-card-preview-text">
|
||||
<Highlight text={childrenText} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Empty preview
|
||||
return null;
|
||||
};
|
||||
|
||||
type ContainerCardProps = {
|
||||
hit: ContainerHit,
|
||||
};
|
||||
@@ -179,7 +224,6 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
|
||||
published,
|
||||
publishStatus,
|
||||
usageKey: containerKey,
|
||||
content,
|
||||
} = hit;
|
||||
|
||||
const numChildrenCount = showOnlyPublished ? (
|
||||
@@ -190,10 +234,6 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
|
||||
showOnlyPublished ? formatted.published?.displayName : formatted.displayName
|
||||
) ?? '';
|
||||
|
||||
const childUsageKeys: Array<string> = (
|
||||
showOnlyPublished ? published?.content?.childUsageKeys : content?.childUsageKeys
|
||||
) ?? [];
|
||||
|
||||
const selected = sidebarComponentInfo?.id === containerKey;
|
||||
|
||||
const { navigateTo } = useLibraryRoutes();
|
||||
@@ -237,7 +277,7 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
|
||||
<BaseCard
|
||||
itemType={itemType}
|
||||
displayName={displayName}
|
||||
preview={<ContainerCardPreview childUsageKeys={childUsageKeys} />}
|
||||
preview={<ContainerCardPreview hit={hit} />}
|
||||
tags={tags}
|
||||
numChildren={numChildrenCount}
|
||||
actions={(
|
||||
|
||||
2
src/library-authoring/components/index.scss
Normal file
2
src/library-authoring/components/index.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "./BaseCard.scss";
|
||||
@import "./ContainerCard.scss";
|
||||
@@ -276,5 +276,10 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Failed to undo remove component operation',
|
||||
description: 'Message to display on failure to undo delete component',
|
||||
},
|
||||
containerPreviewText: {
|
||||
id: 'course-authoring.library-authoring.container.preview.text',
|
||||
defaultMessage: 'Contains {children}.',
|
||||
description: 'Preview message for section/subsections with the names of children separated by commas',
|
||||
},
|
||||
});
|
||||
export default messages;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import "./component-info/ComponentPreview";
|
||||
@import "./components/BaseCard";
|
||||
@import "./components";
|
||||
@import "./generic";
|
||||
@import "./LibraryAuthoringPage";
|
||||
@import "./units";
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface ContentDetails {
|
||||
htmlContent?: string;
|
||||
capaContent?: string;
|
||||
childUsageKeys?: Array<string>;
|
||||
childDisplayNames?: Array<string>;
|
||||
[k: string]: any;
|
||||
}
|
||||
|
||||
@@ -179,6 +180,7 @@ export interface CollectionHit extends BaseContentHit {
|
||||
*/
|
||||
interface ContainerHitContent {
|
||||
childUsageKeys?: string[],
|
||||
childDisplayNames?: string[],
|
||||
}
|
||||
export interface ContainerHit extends BaseContentHit {
|
||||
type: 'library_container';
|
||||
|
||||
Reference in New Issue
Block a user