From be13c18e5d295d4bd7bf9ef1f7d011c7ca36f2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Mon, 9 Jun 2025 11:10:18 -0500 Subject: [PATCH] feat: Section/Subsection Card Preview [FC-0090] (#2057) Section/Subsection card previews --- .../components/ContainerCard.scss | 7 + .../components/ContainerCard.test.tsx | 156 ++++++++++++++++-- .../components/ContainerCard.tsx | 68 ++++++-- src/library-authoring/components/index.scss | 2 + src/library-authoring/components/messages.ts | 5 + src/library-authoring/index.scss | 2 +- src/search-manager/data/api.ts | 2 + 7 files changed, 216 insertions(+), 26 deletions(-) create mode 100644 src/library-authoring/components/ContainerCard.scss create mode 100644 src/library-authoring/components/index.scss diff --git a/src/library-authoring/components/ContainerCard.scss b/src/library-authoring/components/ContainerCard.scss new file mode 100644 index 000000000..f5c6b6bb1 --- /dev/null +++ b/src/library-authoring/components/ContainerCard.scss @@ -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; +}; diff --git a/src/library-authoring/components/ContainerCard.test.tsx b/src/library-authoring/components/ContainerCard.test.tsx index cff6e6bbc..23ca834a8 100644 --- a/src/library-authoring/components/ContainerCard.test.tsx +++ b/src/library-authoring/components/ContainerCard.test.tsx @@ -68,17 +68,51 @@ describe('', () => { ({ axiosMock, mockShowToast } = initializeMocks()); }); - it('should render the card with title', () => { - render(); + 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(); - 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(, 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(, 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('', () => { 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('', () => { 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(); 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('', () => { 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('', () => { 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('', () => { 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(, 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(); + + 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(); + + expect(screen.getByText(displayName)).toBeInTheDocument(); + expect(screen.queryByText(/contains/i)).not.toBeInTheDocument(); + }); }); diff --git a/src/library-authoring/components/ContainerCard.tsx b/src/library-authoring/components/ContainerCard.tsx index 1a4977d6a..ecad91940 100644 --- a/src/library-authoring/components/ContainerCard.tsx +++ b/src/library-authoring/components/ContainerCard.tsx @@ -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 - + {insideCollection && ( @@ -96,7 +96,7 @@ export const ContainerMenu = ({ containerKey, containerType, displayName } : Con )} - + @@ -111,17 +111,17 @@ export const ContainerMenu = ({ containerKey, containerType, displayName } : Con ); }; -type ContainerCardPreviewProps = { - childUsageKeys: Array; +type UnitCardPreviewProps = { + childKeys: Array; 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 ( { - 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 = ( + showOnlyPublished ? published?.content?.childUsageKeys : content?.childUsageKeys + ) ?? []; + + return ; + } + // TODO Section highlights + + const childNames: Array = ( + 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 ( +
+ +
+ ); + } + // 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 = ( - showOnlyPublished ? published?.content?.childUsageKeys : content?.childUsageKeys - ) ?? []; - const selected = sidebarComponentInfo?.id === containerKey; const { navigateTo } = useLibraryRoutes(); @@ -237,7 +277,7 @@ const ContainerCard = ({ hit } : ContainerCardProps) => { } + preview={} tags={tags} numChildren={numChildrenCount} actions={( diff --git a/src/library-authoring/components/index.scss b/src/library-authoring/components/index.scss new file mode 100644 index 000000000..fd6ce4244 --- /dev/null +++ b/src/library-authoring/components/index.scss @@ -0,0 +1,2 @@ +@import "./BaseCard.scss"; +@import "./ContainerCard.scss"; diff --git a/src/library-authoring/components/messages.ts b/src/library-authoring/components/messages.ts index e107eedd2..e2d281034 100644 --- a/src/library-authoring/components/messages.ts +++ b/src/library-authoring/components/messages.ts @@ -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; diff --git a/src/library-authoring/index.scss b/src/library-authoring/index.scss index 3c439e73b..1de953373 100644 --- a/src/library-authoring/index.scss +++ b/src/library-authoring/index.scss @@ -1,5 +1,5 @@ @import "./component-info/ComponentPreview"; -@import "./components/BaseCard"; +@import "./components"; @import "./generic"; @import "./LibraryAuthoringPage"; @import "./units"; diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index 55b3e0bc8..d6cf9db23 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -54,6 +54,7 @@ export interface ContentDetails { htmlContent?: string; capaContent?: string; childUsageKeys?: Array; + childDisplayNames?: Array; [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';