From 98ae74e78c19d5b39e3ee2743db24d2a0495777e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Mon, 31 Mar 2025 12:54:07 -0300 Subject: [PATCH] feat: unit cards in library [FC-0083] (#1742) Unit cards in library and rename BaseComponentCard -> BaseCard --- src/generic/block-type-utils/constants.ts | 2 + .../component-count/ComponentCount.test.tsx | 14 +++ src/generic/component-count/index.tsx | 18 ++++ .../{TagCount.test.jsx => TagCount.test.tsx} | 0 .../tag-count/{index.jsx => index.tsx} | 27 +++--- .../LibraryAuthoringPage.tsx | 2 +- src/library-authoring/LibraryContent.tsx | 25 +++-- .../{ComponentCard.scss => BaseCard.scss} | 10 +- .../{BaseComponentCard.tsx => BaseCard.tsx} | 58 +++++++----- .../components/CollectionCard.test.tsx | 22 ++--- .../components/CollectionCard.tsx | 38 ++++---- .../components/ComponentCard.test.tsx | 2 +- .../components/ComponentCard.tsx | 15 ++- .../components/ContainerCard.test.tsx | 83 +++++++++++++++++ .../components/ContainerCard.tsx | 93 +++++++++++++++++++ src/library-authoring/components/messages.ts | 9 +- src/library-authoring/index.scss | 2 +- src/search-manager/SearchManager.ts | 5 +- src/search-manager/data/api.ts | 21 ++++- src/search-manager/index.ts | 9 +- 20 files changed, 352 insertions(+), 103 deletions(-) create mode 100644 src/generic/component-count/ComponentCount.test.tsx create mode 100644 src/generic/component-count/index.tsx rename src/generic/tag-count/{TagCount.test.jsx => TagCount.test.tsx} (100%) rename src/generic/tag-count/{index.jsx => index.tsx} (54%) rename src/library-authoring/components/{ComponentCard.scss => BaseCard.scss} (72%) rename src/library-authoring/components/{BaseComponentCard.tsx => BaseCard.tsx} (57%) create mode 100644 src/library-authoring/components/ContainerCard.test.tsx create mode 100644 src/library-authoring/components/ContainerCard.tsx diff --git a/src/generic/block-type-utils/constants.ts b/src/generic/block-type-utils/constants.ts index 604519e53..a9a87a0c7 100644 --- a/src/generic/block-type-utils/constants.ts +++ b/src/generic/block-type-utils/constants.ts @@ -56,6 +56,7 @@ export const COMPONENT_TYPE_ICON_MAP: Record = { export const STRUCTURAL_TYPE_ICONS: Record = { vertical: UNIT_TYPE_ICONS_MAP.vertical, + unit: UNIT_TYPE_ICONS_MAP.vertical, sequential: Folder, chapter: Folder, collection: Folder, @@ -73,6 +74,7 @@ export const COMPONENT_TYPE_STYLE_COLOR_MAP = { [COMPONENT_TYPES.video]: 'component-style-video', [COMPONENT_TYPES.dragAndDrop]: 'component-style-default', vertical: 'component-style-vertical', + unit: 'component-style-vertical', sequential: 'component-style-default', chapter: 'component-style-default', collection: 'component-style-collection', diff --git a/src/generic/component-count/ComponentCount.test.tsx b/src/generic/component-count/ComponentCount.test.tsx new file mode 100644 index 000000000..57b37fea7 --- /dev/null +++ b/src/generic/component-count/ComponentCount.test.tsx @@ -0,0 +1,14 @@ +import { render, screen } from '@testing-library/react'; +import ComponentCount from '.'; + +describe('', () => { + it('should render the component', () => { + render(); + expect(screen.getByText('17')).toBeInTheDocument(); + }); + + it('should render the component with zero', () => { + render(); + expect(screen.getByText('0')).toBeInTheDocument(); + }); +}); diff --git a/src/generic/component-count/index.tsx b/src/generic/component-count/index.tsx new file mode 100644 index 000000000..285676407 --- /dev/null +++ b/src/generic/component-count/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Icon, Stack } from '@openedx/paragon'; +import { Widgets } from '@openedx/paragon/icons'; + +type ComponentCountProps = { + count?: number; +}; + +const ComponentCount: React.FC = ({ count }) => ( + count !== undefined ? ( + + + {count} + + ) : null +); + +export default ComponentCount; diff --git a/src/generic/tag-count/TagCount.test.jsx b/src/generic/tag-count/TagCount.test.tsx similarity index 100% rename from src/generic/tag-count/TagCount.test.jsx rename to src/generic/tag-count/TagCount.test.tsx diff --git a/src/generic/tag-count/index.jsx b/src/generic/tag-count/index.tsx similarity index 54% rename from src/generic/tag-count/index.jsx rename to src/generic/tag-count/index.tsx index bb6dada9d..d8787bb2b 100644 --- a/src/generic/tag-count/index.jsx +++ b/src/generic/tag-count/index.tsx @@ -1,14 +1,20 @@ -import PropTypes from 'prop-types'; -import { Icon, Button } from '@openedx/paragon'; +import { Button, Icon, Stack } from '@openedx/paragon'; import { Tag } from '@openedx/paragon/icons'; import classNames from 'classnames'; -const TagCount = ({ count, onClick }) => { +type TagCountProps = { + count: number; + onClick?: () => void; + size?: Parameters[0]['size']; +}; + +// eslint-disable-next-line react/prop-types +const TagCount: React.FC = ({ count, onClick, size }) => { const renderContent = () => ( - <> - + + {count} - + ); return ( @@ -26,13 +32,4 @@ const TagCount = ({ count, onClick }) => { ); }; -TagCount.defaultProps = { - onClick: undefined, -}; - -TagCount.propTypes = { - count: PropTypes.number.isRequired, - onClick: PropTypes.func, -}; - export default TagCount; diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index f40f187e5..7c4c54f38 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -215,7 +215,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage } const activeTypeFilters = { - components: 'NOT type = "collection"', + components: 'type = "library_block"', collections: 'type = "collection"', }; if (activeKey !== ContentType.home) { diff --git a/src/library-authoring/LibraryContent.tsx b/src/library-authoring/LibraryContent.tsx index 1913994b2..53fdb23ec 100644 --- a/src/library-authoring/LibraryContent.tsx +++ b/src/library-authoring/LibraryContent.tsx @@ -6,6 +6,7 @@ import { useLibraryContext } from './common/context/LibraryContext'; import { useSidebarContext } from './common/context/SidebarContext'; import CollectionCard from './components/CollectionCard'; import ComponentCard from './components/ComponentCard'; +import ContainerCard from './components/ContainerCard'; import { ContentType } from './routes'; import { useLoadOnScroll } from '../hooks'; import messages from './collections/messages'; @@ -22,6 +23,12 @@ type LibraryContentProps = { contentType?: ContentType; }; +const LibraryItemCard = { + collection: CollectionCard, + library_block: ComponentCard, + library_container: ContainerCard, +}; + const LibraryContent = ({ contentType = ContentType.home }: LibraryContentProps) => { const { hits, @@ -69,19 +76,11 @@ const LibraryContent = ({ contentType = ContentType.home }: LibraryContentProps) return (
- {hits.map((contentHit) => ( - contentHit.type === 'collection' ? ( - - ) : ( - - ) - ))} + {hits.map((contentHit) => { + const CardComponent = LibraryItemCard[contentHit.type] || ComponentCard; + + return ; + })}
); }; diff --git a/src/library-authoring/components/ComponentCard.scss b/src/library-authoring/components/BaseCard.scss similarity index 72% rename from src/library-authoring/components/ComponentCard.scss rename to src/library-authoring/components/BaseCard.scss index cdf72300e..9346618a3 100644 --- a/src/library-authoring/components/ComponentCard.scss +++ b/src/library-authoring/components/BaseCard.scss @@ -1,14 +1,14 @@ -.library-component-card { +.library-item-card { .pgn__card { height: 100% } - .library-component-header { + .library-item-header { border-top-left-radius: .375rem; border-top-right-radius: .375rem; padding: 0 .5rem 0 1.25rem; - .library-component-header-icon { + .library-item-header-icon { width: 2.3rem; height: 2.3rem; } @@ -21,4 +21,8 @@ margin: .25rem 0 .25rem 1rem; } } + + .badge-container { + min-height: 20px; + } } diff --git a/src/library-authoring/components/BaseComponentCard.tsx b/src/library-authoring/components/BaseCard.tsx similarity index 57% rename from src/library-authoring/components/BaseComponentCard.tsx rename to src/library-authoring/components/BaseCard.tsx index 0c52c0a88..f327c7801 100644 --- a/src/library-authoring/components/BaseComponentCard.tsx +++ b/src/library-authoring/components/BaseCard.tsx @@ -9,13 +9,14 @@ import { import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils'; +import ComponentCount from '../../generic/component-count'; import TagCount from '../../generic/tag-count'; import { BlockTypeLabel, type ContentHitTags, Highlight } from '../../search-manager'; -type BaseComponentCardProps = { - componentType: string; +type BaseCardProps = { + itemType: string; displayName: string; - description: string; + description?: string; numChildren?: number; tags: ContentHitTags; actions: React.ReactNode; @@ -23,16 +24,16 @@ type BaseComponentCardProps = { onSelect: () => void }; -const BaseComponentCard = ({ - componentType, +const BaseCard = ({ + itemType, displayName, - description, + description = '', numChildren, tags, actions, onSelect, ...props -} : BaseComponentCardProps) => { +} : BaseCardProps) => { const tagCount = useMemo(() => { if (!tags) { return 0; @@ -41,11 +42,11 @@ const BaseComponentCard = ({ + (tags.level2?.length || 0) + (tags.level3?.length || 0); }, [tags]); - const componentIcon = getItemIcon(componentType); + const itemIcon = getItemIcon(itemType); const intl = useIntl(); return ( - + + } actions={ // Wrap the actions in a div to prevent the card from being clicked when the actions are clicked @@ -67,27 +68,36 @@ const BaseComponentCard = ({
e.stopPropagation()}>{actions}
} /> - + - - - - - - - - -
-
- {props.hasUnpublishedChanges ? {intl.formatMessage(messages.unpublishedChanges)} : null} +
+ + + + + + + + + + + + +
+ {props.hasUnpublishedChanges && ( + {intl.formatMessage(messages.unpublishedChanges)} + )} +
+
+
); }; -export default BaseComponentCard; +export default BaseCard; diff --git a/src/library-authoring/components/CollectionCard.test.tsx b/src/library-authoring/components/CollectionCard.test.tsx index 3f7dc7fc8..2cb43481f 100644 --- a/src/library-authoring/components/CollectionCard.test.tsx +++ b/src/library-authoring/components/CollectionCard.test.tsx @@ -10,7 +10,7 @@ import CollectionCard from './CollectionCard'; import messages from './messages'; import { getLibraryCollectionApiUrl, getLibraryCollectionRestoreApiUrl } from '../data/api'; -const CollectionHitSample: CollectionHit = { +const collectionHitSample: CollectionHit = { id: 'lib-collectionorg1democourse-collection-display-name', type: 'collection', contextKey: 'lb:org1:Demo_Course', @@ -55,23 +55,23 @@ describe('', () => { }); it('should render the card with title and description', () => { - render(); + render(); expect(screen.queryByText('Collection Display Formated Name')).toBeInTheDocument(); expect(screen.queryByText('Collection description')).toBeInTheDocument(); - expect(screen.queryByText('Collection (2)')).toBeInTheDocument(); + expect(screen.queryByText('2')).toBeInTheDocument(); // Component count }); it('should render published content', () => { - render(, true); + render(, true); expect(screen.queryByText('Collection Display Formated Name')).toBeInTheDocument(); expect(screen.queryByText('Collection description')).toBeInTheDocument(); - expect(screen.queryByText('Collection (1)')).toBeInTheDocument(); + expect(screen.queryByText('1')).toBeInTheDocument(); // Published Component Count }); it('should navigate to the collection if the open menu clicked', async () => { - render(); + render(); // Open menu expect(screen.getByTestId('collection-card-menu-toggle')).toBeInTheDocument(); @@ -85,9 +85,9 @@ describe('', () => { }); it('should show confirmation box, delete collection and show toast to undo deletion', async () => { - const url = getLibraryCollectionApiUrl(CollectionHitSample.contextKey, CollectionHitSample.blockId); + const url = getLibraryCollectionApiUrl(collectionHitSample.contextKey, collectionHitSample.blockId); axiosMock.onDelete(url).reply(204); - render(); + render(); expect(screen.queryByText('Collection Display Formated Name')).toBeInTheDocument(); // Open menu @@ -123,7 +123,7 @@ describe('', () => { // Get restore / undo func from the toast const restoreFn = mockShowToast.mock.calls[0][1].onClick; - const restoreUrl = getLibraryCollectionRestoreApiUrl(CollectionHitSample.contextKey, CollectionHitSample.blockId); + const restoreUrl = getLibraryCollectionRestoreApiUrl(collectionHitSample.contextKey, collectionHitSample.blockId); axiosMock.onPost(restoreUrl).reply(200); // restore collection restoreFn(); @@ -134,9 +134,9 @@ describe('', () => { }); it('should show failed toast on delete collection failure', async () => { - const url = getLibraryCollectionApiUrl(CollectionHitSample.contextKey, CollectionHitSample.blockId); + const url = getLibraryCollectionApiUrl(collectionHitSample.contextKey, collectionHitSample.blockId); axiosMock.onDelete(url).reply(404); - render(); + render(); expect(screen.queryByText('Collection Display Formated Name')).toBeInTheDocument(); // Open menu diff --git a/src/library-authoring/components/CollectionCard.tsx b/src/library-authoring/components/CollectionCard.tsx index ee165e04a..7067d5009 100644 --- a/src/library-authoring/components/CollectionCard.tsx +++ b/src/library-authoring/components/CollectionCard.tsx @@ -15,23 +15,29 @@ import { useComponentPickerContext } from '../common/context/ComponentPickerCont import { useLibraryContext } from '../common/context/LibraryContext'; import { useSidebarContext } from '../common/context/SidebarContext'; import { useLibraryRoutes } from '../routes'; -import BaseComponentCard from './BaseComponentCard'; +import BaseCard from './BaseCard'; import { ToastContext } from '../../generic/toast-context'; import { useDeleteCollection, useRestoreCollection } from '../data/apiHooks'; import DeleteModal from '../../generic/delete-modal/DeleteModal'; import messages from './messages'; type CollectionMenuProps = { - collectionHit: CollectionHit, + hit: CollectionHit, }; -const CollectionMenu = ({ collectionHit } : CollectionMenuProps) => { +const CollectionMenu = ({ hit } : CollectionMenuProps) => { const intl = useIntl(); const { showToast } = useContext(ToastContext); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false); const { closeLibrarySidebar, sidebarComponentInfo } = useSidebarContext(); + const { + contextKey, + blockId, + type, + displayName, + } = hit; - const restoreCollectionMutation = useRestoreCollection(collectionHit.contextKey, collectionHit.blockId); + const restoreCollectionMutation = useRestoreCollection(contextKey, blockId); const restoreCollection = useCallback(() => { restoreCollectionMutation.mutateAsync() .then(() => { @@ -41,9 +47,9 @@ const CollectionMenu = ({ collectionHit } : CollectionMenuProps) => { }); }, []); - const deleteCollectionMutation = useDeleteCollection(collectionHit.contextKey, collectionHit.blockId); + const deleteCollectionMutation = useDeleteCollection(contextKey, blockId); const deleteCollection = useCallback(async () => { - if (sidebarComponentInfo?.id === collectionHit.blockId) { + if (sidebarComponentInfo?.id === blockId) { // Close sidebar if current collection is open to avoid displaying // deleted collection in sidebar closeLibrarySidebar(); @@ -79,7 +85,7 @@ const CollectionMenu = ({ collectionHit } : CollectionMenuProps) => { @@ -92,9 +98,9 @@ const CollectionMenu = ({ collectionHit } : CollectionMenuProps) => { isOpen={isDeleteModalOpen} close={closeDeleteModal} variant="warning" - category={collectionHit.type} + category={type} description={intl.formatMessage(messages.deleteCollectionConfirm, { - collectionTitle: collectionHit.displayName, + collectionTitle: displayName, })} onDeleteSubmit={deleteCollection} /> @@ -103,22 +109,22 @@ const CollectionMenu = ({ collectionHit } : CollectionMenuProps) => { }; type CollectionCardProps = { - collectionHit: CollectionHit, + hit: CollectionHit, }; -const CollectionCard = ({ collectionHit } : CollectionCardProps) => { +const CollectionCard = ({ hit } : CollectionCardProps) => { const { componentPickerMode } = useComponentPickerContext(); const { showOnlyPublished } = useLibraryContext(); const { openCollectionInfoSidebar } = useSidebarContext(); const { - type: componentType, + type: itemType, blockId: collectionId, formatted, tags, numChildren, published, - } = collectionHit; + } = hit; const numChildrenCount = showOnlyPublished ? ( published?.numChildren || 0 @@ -136,15 +142,15 @@ const CollectionCard = ({ collectionHit } : CollectionCardProps) => { }, [collectionId, navigateTo, openCollectionInfoSidebar]); return ( - - + )} onSelect={openCollection} diff --git a/src/library-authoring/components/ComponentCard.test.tsx b/src/library-authoring/components/ComponentCard.test.tsx index c6e5ba5c9..adea38568 100644 --- a/src/library-authoring/components/ComponentCard.test.tsx +++ b/src/library-authoring/components/ComponentCard.test.tsx @@ -47,7 +47,7 @@ const clipboardBroadcastChannelMock = { (global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock); const libraryId = 'lib:org1:Demo_Course'; -const render = () => baseRender(, { +const render = () => baseRender(, { extraWrapper: ({ children }) => ( { children } diff --git a/src/library-authoring/components/ComponentCard.tsx b/src/library-authoring/components/ComponentCard.tsx index e482cf7d8..672a48b8b 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -17,21 +17,20 @@ import { import { useClipboard } from '../../generic/clipboard'; import { ToastContext } from '../../generic/toast-context'; -import { type ContentHit } from '../../search-manager'; +import { type ContentHit, PublishStatus } from '../../search-manager'; import { useComponentPickerContext } from '../common/context/ComponentPickerContext'; import { useLibraryContext } from '../common/context/LibraryContext'; import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext'; import { useRemoveComponentsFromCollection } from '../data/apiHooks'; import { useLibraryRoutes } from '../routes'; -import BaseComponentCard from './BaseComponentCard'; +import BaseCard from './BaseCard'; import { canEditComponent } from './ComponentEditorModal'; import messages from './messages'; import ComponentDeleter from './ComponentDeleter'; -import { PublishStatus } from '../../search-manager/data/api'; type ComponentCardProps = { - contentHit: ContentHit, + hit: ContentHit, }; export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { @@ -181,7 +180,7 @@ const AddComponentWidget = ({ usageKey, blockType }: AddComponentWidgetProps) => return null; }; -const ComponentCard = ({ contentHit }: ComponentCardProps) => { +const ComponentCard = ({ hit }: ComponentCardProps) => { const { showOnlyPublished } = useLibraryContext(); const { openComponentInfoSidebar } = useSidebarContext(); const { componentPickerMode } = useComponentPickerContext(); @@ -192,7 +191,7 @@ const ComponentCard = ({ contentHit }: ComponentCardProps) => { tags, usageKey, publishStatus, - } = contentHit; + } = hit; const componentDescription: string = ( showOnlyPublished ? formatted.published?.description : formatted.description ) ?? ''; @@ -210,8 +209,8 @@ const ComponentCard = ({ contentHit }: ComponentCardProps) => { }, [usageKey, navigateTo, openComponentInfoSidebar]); return ( - baseRender(ui, { + extraWrapper: ({ children }) => ( + + {children} + + ), +}); + +describe('', () => { + beforeEach(() => { + initializeMocks(); + }); + + it('should render the card with title', () => { + render(); + + expect(screen.queryByText('Unit Display Formated Name')).toBeInTheDocument(); + expect(screen.queryByText('2')).toBeInTheDocument(); // Component count + }); + + it('should render published content', () => { + render(, true); + + expect(screen.queryByText('Published Unit Display Name')).toBeInTheDocument(); + expect(screen.queryByText('1')).toBeInTheDocument(); // Published Component Count + }); + + it('should navigate to the container if the open menu clicked', async () => { + render(); + + // Open menu + expect(screen.getByTestId('container-card-menu-toggle')).toBeInTheDocument(); + userEvent.click(screen.getByTestId('container-card-menu-toggle')); + + // Open menu item + const openMenuItem = screen.getByRole('link', { name: 'Open' }); + expect(openMenuItem).toBeInTheDocument(); + + // TODO: To be implemented + // expect(openMenuItem).toHaveAttribute( + // 'href', + // '/library/lb:org1:Demo_Course/container/container-display-name-123', + // ); + }); +}); diff --git a/src/library-authoring/components/ContainerCard.tsx b/src/library-authoring/components/ContainerCard.tsx new file mode 100644 index 000000000..7855d82cd --- /dev/null +++ b/src/library-authoring/components/ContainerCard.tsx @@ -0,0 +1,93 @@ +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; +import { + ActionRow, + Dropdown, + Icon, + IconButton, +} from '@openedx/paragon'; +import { MoreVert } from '@openedx/paragon/icons'; +import { Link } from 'react-router-dom'; + +import { type ContainerHit, PublishStatus } from '../../search-manager'; +import { useComponentPickerContext } from '../common/context/ComponentPickerContext'; +import { useLibraryContext } from '../common/context/LibraryContext'; +import BaseCard from './BaseCard'; +import messages from './messages'; + +type ContainerMenuProps = { + hit: ContainerHit, +}; + +const ContainerMenu = ({ hit } : ContainerMenuProps) => { + const intl = useIntl(); + const { contextKey, blockId } = hit; + + return ( + + + + + + + + + ); +}; + +type ContainerCardProps = { + hit: ContainerHit, +}; + +const ContainerCard = ({ hit } : ContainerCardProps) => { + const { componentPickerMode } = useComponentPickerContext(); + const { showOnlyPublished } = useLibraryContext(); + + const { + blockType: itemType, + formatted, + tags, + numChildren, + published, + publishStatus, + } = hit; + + const numChildrenCount = showOnlyPublished ? ( + published?.numChildren || 0 + ) : numChildren; + + const displayName: string = ( + showOnlyPublished ? formatted.published?.displayName : formatted.displayName + ) ?? ''; + + const openContainer = () => {}; + + return ( + + + + )} + hasUnpublishedChanges={publishStatus !== PublishStatus.Published} + onSelect={openContainer} + /> + ); +}; + +export default ContainerCard; diff --git a/src/library-authoring/components/messages.ts b/src/library-authoring/components/messages.ts index 0cb32d195..40276ca7c 100644 --- a/src/library-authoring/components/messages.ts +++ b/src/library-authoring/components/messages.ts @@ -11,10 +11,15 @@ const messages = defineMessages({ defaultMessage: 'Collection actions menu', description: 'Alt/title text for the collection card menu button.', }, + containerCardMenuAlt: { + id: 'course-authoring.library-authoring.container.menu', + defaultMessage: 'Container actions menu', + description: 'Alt/title text for the container card menu button.', + }, menuOpen: { - id: 'course-authoring.library-authoring.collection.menu.open', + id: 'course-authoring.library-authoring.menu.open', defaultMessage: 'Open', - description: 'Menu item for open a collection.', + description: 'Menu item for open a collection/container.', }, menuEdit: { id: 'course-authoring.library-authoring.component.menu.edit', diff --git a/src/library-authoring/index.scss b/src/library-authoring/index.scss index 62097ddbc..fcfb6732a 100644 --- a/src/library-authoring/index.scss +++ b/src/library-authoring/index.scss @@ -1,5 +1,5 @@ @import "./component-info/ComponentPreview"; -@import "./components/ComponentCard"; +@import "./components/BaseCard"; @import "./generic"; @import "./LibraryAuthoringPage"; diff --git a/src/search-manager/SearchManager.ts b/src/search-manager/SearchManager.ts index f9fb7a236..d53ceb675 100644 --- a/src/search-manager/SearchManager.ts +++ b/src/search-manager/SearchManager.ts @@ -9,8 +9,7 @@ import { MeiliSearch, type Filter } from 'meilisearch'; import { union } from 'lodash'; import { - CollectionHit, - ContentHit, + type HitType, SearchSortOption, forceArray, PublishStatus, } from './data/api'; @@ -39,7 +38,7 @@ export interface SearchContextData { searchSortOrder: SearchSortOption; setSearchSortOrder: React.Dispatch>; defaultSearchSortOrder: SearchSortOption; - hits: (ContentHit | CollectionHit)[]; + hits: HitType[]; totalHits: number; isLoading: boolean; hasNextPage: boolean | undefined; diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index 99dbcfa59..549054b3f 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -105,7 +105,7 @@ export interface ContentHitTags { */ interface BaseContentHit { id: string; - type: 'course_block' | 'library_block' | 'collection'; + type: 'course_block' | 'library_block' | 'collection' | 'library_container'; displayName: string; usageKey: string; blockId: string; @@ -167,11 +167,26 @@ export interface CollectionHit extends BaseContentHit { published?: ContentPublishedData; } +/** + * Information about a single container returned in the search results + * Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py + */ +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, }; +} + +export type HitType = ContentHit | CollectionHit | ContainerHit; + /** * Convert search hits to camelCase * @param hit A search result directly from Meilisearch */ -export function formatSearchHit(hit: Record): ContentHit | CollectionHit { +export function formatSearchHit(hit: Record): HitType { // eslint-disable-next-line @typescript-eslint/naming-convention const { _formatted, ...newHit } = hit; newHit.formatted = { @@ -214,7 +229,7 @@ export async function fetchSearchResults({ skipBlockTypeFetch = false, limit = 20, }: FetchSearchParams): Promise<{ - hits: (ContentHit | CollectionHit)[], + hits: HitType[], nextOffset: number | undefined, totalHits: number, blockTypes: Record, diff --git a/src/search-manager/index.ts b/src/search-manager/index.ts index cd73551f0..bb4fed411 100644 --- a/src/search-manager/index.ts +++ b/src/search-manager/index.ts @@ -8,8 +8,13 @@ export { default as Highlight } from './Highlight'; export { default as SearchKeywordsField } from './SearchKeywordsField'; export { default as SearchSortWidget } from './SearchSortWidget'; export { default as Stats } from './Stats'; -export { HIGHLIGHT_PRE_TAG, HIGHLIGHT_POST_TAG } from './data/api'; +export { HIGHLIGHT_PRE_TAG, HIGHLIGHT_POST_TAG, PublishStatus } from './data/api'; export { useContentSearchConnection, useContentSearchResults, useGetBlockTypes } from './data/apiHooks'; export { TypesFilterData } from './hooks'; -export type { CollectionHit, ContentHit, ContentHitTags } from './data/api'; +export type { + CollectionHit, + ContainerHit, + ContentHit, + ContentHitTags, +} from './data/api';