diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 8a439463d..9fc69521a 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -14,7 +14,6 @@ import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json' import { mockContentLibrary, mockGetCollectionMetadata, - mockLibraryBlockTypes, mockXBlockFields, } from './data/api.mocks'; import { mockContentSearchConfig } from '../search-manager/data/api.mock'; @@ -25,7 +24,6 @@ import { getLibraryCollectionsApiUrl } from './data/api'; mockGetCollectionMetadata.applyMock(); mockContentSearchConfig.applyMock(); mockContentLibrary.applyMock(); -mockLibraryBlockTypes.applyMock(); mockXBlockFields.applyMock(); mockBroadcastChannel(); diff --git a/src/library-authoring/LibraryRecentlyModified.tsx b/src/library-authoring/LibraryRecentlyModified.tsx index 4d66cb880..5577d4587 100644 --- a/src/library-authoring/LibraryRecentlyModified.tsx +++ b/src/library-authoring/LibraryRecentlyModified.tsx @@ -1,4 +1,3 @@ -import React, { useMemo } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { orderBy } from 'lodash'; @@ -7,7 +6,6 @@ import { type CollectionHit, type ContentHit, SearchSortOption } from '../search import LibrarySection, { LIBRARY_SECTION_PREVIEW_LIMIT } from './components/LibrarySection'; import messages from './messages'; import ComponentCard from './components/ComponentCard'; -import { useLibraryBlockTypes } from './data/apiHooks'; import CollectionCard from './components/CollectionCard'; import { useLibraryContext } from './common/context'; @@ -19,7 +17,6 @@ const RecentlyModified: React.FC> = () => { totalHits, totalCollectionHits, } = useSearchContext(); - const { libraryId } = useLibraryContext(); const componentCount = totalHits + totalCollectionHits; // Since we only display a fixed number of items in preview, @@ -32,17 +29,6 @@ const RecentlyModified: React.FC> = () => { ...collectionList, ], ['modified'], ['desc']).slice(0, LIBRARY_SECTION_PREVIEW_LIMIT); - const { data: blockTypesData } = useLibraryBlockTypes(libraryId); - const blockTypes = useMemo(() => { - const result = {}; - if (blockTypesData) { - blockTypesData.forEach(blockType => { - result[blockType.blockType] = blockType; - }); - } - return result; - }, [blockTypesData]); - return componentCount > 0 ? ( > = () => { ) ))} diff --git a/src/library-authoring/add-content/AddContentWorkflow.test.tsx b/src/library-authoring/add-content/AddContentWorkflow.test.tsx index d16f8a644..bd464f39b 100644 --- a/src/library-authoring/add-content/AddContentWorkflow.test.tsx +++ b/src/library-authoring/add-content/AddContentWorkflow.test.tsx @@ -15,7 +15,6 @@ import * as textEditorHooks from '../../editors/containers/TextEditor/hooks'; import { mockContentLibrary, mockCreateLibraryBlock, - mockLibraryBlockTypes, mockXBlockFields, } from '../data/api.mocks'; import { mockBroadcastChannel, mockClipboardEmpty } from '../../generic/data/api.mock'; @@ -23,7 +22,6 @@ import { mockContentSearchConfig, mockSearchResult } from '../../search-manager/ import LibraryLayout from '../LibraryLayout'; mockContentSearchConfig.applyMock(); -mockLibraryBlockTypes.applyMock(); mockClipboardEmpty.applyMock(); mockBroadcastChannel(); mockContentLibrary.applyMock(); diff --git a/src/library-authoring/collections/CollectionDetails.tsx b/src/library-authoring/collections/CollectionDetails.tsx index 993617790..87c8b6332 100644 --- a/src/library-authoring/collections/CollectionDetails.tsx +++ b/src/library-authoring/collections/CollectionDetails.tsx @@ -84,7 +84,7 @@ const CollectionStatsWidget = ({ libraryId, collectionId }: CollectionStatsWidge {blockTypesArray.map(({ blockType, count }) => ( } + label={} blockType={blockType} count={count} /> diff --git a/src/library-authoring/collections/LibraryCollectionPage.test.tsx b/src/library-authoring/collections/LibraryCollectionPage.test.tsx index 67e4eea08..c1cfb9602 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.test.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.test.tsx @@ -11,7 +11,6 @@ import { import mockResult from '../__mocks__/collection-search.json'; import { mockContentLibrary, - mockLibraryBlockTypes, mockXBlockFields, mockGetCollectionMetadata, } from '../data/api.mocks'; @@ -24,7 +23,6 @@ mockGetCollectionMetadata.applyMock(); mockContentSearchConfig.applyMock(); mockGetBlockTypes.applyMock(); mockContentLibrary.applyMock(); -mockLibraryBlockTypes.applyMock(); mockXBlockFields.applyMock(); mockBroadcastChannel(); diff --git a/src/library-authoring/components/BaseComponentCard.tsx b/src/library-authoring/components/BaseComponentCard.tsx index 1cd77ee7f..905d76d5a 100644 --- a/src/library-authoring/components/BaseComponentCard.tsx +++ b/src/library-authoring/components/BaseComponentCard.tsx @@ -8,25 +8,25 @@ import { import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils'; import TagCount from '../../generic/tag-count'; -import { ContentHitTags, Highlight } from '../../search-manager'; +import { BlockTypeLabel, ContentHitTags, Highlight } from '../../search-manager'; type BaseComponentCardProps = { - type: string, + componentType: string, displayName: string, description: string, + numChildren?: number, tags: ContentHitTags, actions: React.ReactNode, - blockTypeDisplayName: string, openInfoSidebar: () => void }; const BaseComponentCard = ({ - type, + componentType, displayName, description, + numChildren, tags, actions, - blockTypeDisplayName, openInfoSidebar, } : BaseComponentCardProps) => { const tagCount = useMemo(() => { @@ -37,7 +37,7 @@ const BaseComponentCard = ({ + (tags.level2?.length || 0) + (tags.level3?.length || 0); }, [tags]); - const componentIcon = getItemIcon(type); + const componentIcon = getItemIcon(componentType); return ( @@ -51,7 +51,7 @@ const BaseComponentCard = ({ }} > } @@ -62,7 +62,9 @@ const BaseComponentCard = ({ - {blockTypeDisplayName} + + + diff --git a/src/library-authoring/components/CollectionCard.tsx b/src/library-authoring/components/CollectionCard.tsx index c10661230..1d81e29dc 100644 --- a/src/library-authoring/components/CollectionCard.tsx +++ b/src/library-authoring/components/CollectionCard.tsx @@ -44,35 +44,30 @@ type CollectionCardProps = { }; const CollectionCard = ({ collectionHit }: CollectionCardProps) => { - const intl = useIntl(); const { openCollectionInfoSidebar, } = useLibraryContext(); const { - type, + type: componentType, formatted, tags, numChildren, } = collectionHit; const { displayName = '', description = '' } = formatted; - const blockTypeDisplayName = numChildren ? intl.formatMessage( - messages.collectionTypeWithCount, - { numChildren }, - ) : intl.formatMessage(messages.collectionType); return ( )} - blockTypeDisplayName={blockTypeDisplayName} openInfoSidebar={() => openCollectionInfoSidebar(collectionHit.blockId)} /> ); diff --git a/src/library-authoring/components/ComponentCard.test.tsx b/src/library-authoring/components/ComponentCard.test.tsx index 1196c1820..b22c6b028 100644 --- a/src/library-authoring/components/ComponentCard.test.tsx +++ b/src/library-authoring/components/ComponentCard.test.tsx @@ -42,7 +42,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 a1314544d..6e16937e8 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -20,7 +20,6 @@ import BaseComponentCard from './BaseComponentCard'; type ComponentCardProps = { contentHit: ContentHit, - blockTypeDisplayName: string, }; export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { @@ -63,7 +62,7 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { ); }; -const ComponentCard = ({ contentHit, blockTypeDisplayName } : ComponentCardProps) => { +const ComponentCard = ({ contentHit } : ComponentCardProps) => { const { openComponentInfoSidebar, } = useLibraryContext(); @@ -83,7 +82,7 @@ const ComponentCard = ({ contentHit, blockTypeDisplayName } : ComponentCardProps return ( )} - blockTypeDisplayName={blockTypeDisplayName} openInfoSidebar={() => openComponentInfoSidebar(usageKey)} /> ); diff --git a/src/library-authoring/components/LibraryComponents.test.tsx b/src/library-authoring/components/LibraryComponents.test.tsx index 0bf198063..ac1a58a67 100644 --- a/src/library-authoring/components/LibraryComponents.test.tsx +++ b/src/library-authoring/components/LibraryComponents.test.tsx @@ -7,7 +7,7 @@ import { initializeMocks, } from '../../testUtils'; import { getContentSearchConfigUrl } from '../../search-manager/data/api'; -import { mockLibraryBlockTypes, mockContentLibrary } from '../data/api.mocks'; +import { mockContentLibrary } from '../data/api.mocks'; import mockEmptyResult from '../../search-modal/__mocks__/empty-search-result.json'; import { LibraryProvider } from '../common/context'; import { libraryComponentsMock } from '../__mocks__'; @@ -15,7 +15,6 @@ import LibraryComponents from './LibraryComponents'; const searchEndpoint = 'http://mock.meilisearch.local/multi-search'; -mockLibraryBlockTypes.applyMock(); mockContentLibrary.applyMock(); const mockFetchNextPage = jest.fn(); const mockUseSearchContext = jest.fn(); diff --git a/src/library-authoring/components/LibraryComponents.tsx b/src/library-authoring/components/LibraryComponents.tsx index 8f44e7762..c260897b6 100644 --- a/src/library-authoring/components/LibraryComponents.tsx +++ b/src/library-authoring/components/LibraryComponents.tsx @@ -1,10 +1,7 @@ -import React, { useMemo } from 'react'; - import { LoadingSpinner } from '../../generic/Loading'; import { useLoadOnScroll } from '../../hooks'; import { useSearchContext } from '../../search-manager'; import { NoComponents, NoSearchResults } from '../EmptyStates'; -import { useLibraryBlockTypes } from '../data/apiHooks'; import ComponentCard from './ComponentCard'; import { LIBRARY_SECTION_PREVIEW_LIMIT } from './LibrarySection'; import { useLibraryContext } from '../common/context'; @@ -30,22 +27,10 @@ const LibraryComponents = ({ variant }: LibraryComponentsProps) => { isLoading, isFiltered, } = useSearchContext(); - const { libraryId, openAddContentSidebar } = useLibraryContext(); + const { openAddContentSidebar } = useLibraryContext(); const componentList = variant === 'preview' ? hits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT) : hits; - // TODO get rid of "useLibraryBlockTypes". Use instead. - const { data: blockTypesData } = useLibraryBlockTypes(libraryId); - const blockTypes = useMemo(() => { - const result = {}; - if (blockTypesData) { - blockTypesData.forEach(blockType => { - result[blockType.blockType] = blockType; - }); - } - return result; - }, [blockTypesData]); - useLoadOnScroll( hasNextPage, isFetchingNextPage, @@ -67,7 +52,6 @@ const LibraryComponents = ({ variant }: LibraryComponentsProps) => { )) } diff --git a/src/library-authoring/components/messages.ts b/src/library-authoring/components/messages.ts index c826bae09..ef2b89fe3 100644 --- a/src/library-authoring/components/messages.ts +++ b/src/library-authoring/components/messages.ts @@ -11,16 +11,6 @@ const messages = defineMessages({ defaultMessage: 'Collection actions menu', description: 'Alt/title text for the collection card menu button.', }, - collectionType: { - id: 'course-authoring.library-authoring.collection.type', - defaultMessage: 'Collection', - description: 'Collection type text', - }, - collectionTypeWithCount: { - id: 'course-authoring.library-authoring.collection.type-with-count', - defaultMessage: 'Collection ({numChildren})', - description: 'Collection type text with children count', - }, menuOpen: { id: 'course-authoring.library-authoring.collection.menu.open', defaultMessage: 'Open', diff --git a/src/library-authoring/data/api.mocks.ts b/src/library-authoring/data/api.mocks.ts index 220c7576d..c4ae60284 100644 --- a/src/library-authoring/data/api.mocks.ts +++ b/src/library-authoring/data/api.mocks.ts @@ -3,49 +3,6 @@ import { mockContentTaxonomyTagsData } from '../../content-tags-drawer/data/api. import { createAxiosError } from '../../testUtils'; import * as api from './api'; -/** - * Mock for `getLibraryBlockTypes()` - */ -export async function mockLibraryBlockTypes(): Promise { - return [ - { blockType: 'about', displayName: 'overview' }, - { blockType: 'annotatable', displayName: 'Annotation' }, - { blockType: 'chapter', displayName: 'Section' }, - { blockType: 'conditional', displayName: 'Conditional' }, - { blockType: 'course', displayName: 'Empty' }, - { blockType: 'course_info', displayName: 'Text' }, - { blockType: 'discussion', displayName: 'Discussion' }, - { blockType: 'done', displayName: 'Completion' }, - { blockType: 'drag-and-drop-v2', displayName: 'Drag and Drop' }, - { blockType: 'edx_sga', displayName: 'Staff Graded Assignment' }, - { blockType: 'google-calendar', displayName: 'Google Calendar' }, - { blockType: 'google-document', displayName: 'Google Document' }, - { blockType: 'html', displayName: 'Text' }, - { blockType: 'library', displayName: 'Library' }, - { blockType: 'library_content', displayName: 'Randomized Content Block' }, - { blockType: 'lti', displayName: 'LTI' }, - { blockType: 'lti_consumer', displayName: 'LTI Consumer' }, - { blockType: 'openassessment', displayName: 'Open Response Assessment' }, - { blockType: 'poll', displayName: 'Poll' }, - { blockType: 'problem', displayName: 'Problem' }, - { blockType: 'scorm', displayName: 'Scorm module' }, - { blockType: 'sequential', displayName: 'Subsection' }, - { blockType: 'split_test', displayName: 'Content Experiment' }, - { blockType: 'staffgradedxblock', displayName: 'Staff Graded Points' }, - { blockType: 'static_tab', displayName: 'Empty' }, - { blockType: 'survey', displayName: 'Survey' }, - { blockType: 'thumbs', displayName: 'Thumbs' }, - { blockType: 'unit', displayName: 'Unit' }, - { blockType: 'vertical', displayName: 'Unit' }, - { blockType: 'video', displayName: 'Video' }, - { blockType: 'videoalpha', displayName: 'Video' }, - { blockType: 'word_cloud', displayName: 'Word cloud' }, - ]; -} -mockLibraryBlockTypes.applyMock = () => { - jest.spyOn(api, 'getLibraryBlockTypes').mockImplementation(mockLibraryBlockTypes); -}; - /** * Mock for `getContentLibrary()` * diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts index 5c9617676..225acddb8 100644 --- a/src/library-authoring/data/api.ts +++ b/src/library-authoring/data/api.ts @@ -8,11 +8,6 @@ const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; */ export const getContentLibraryApiUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/`; -/** - * Get the URL for getting block types of a library (what types can be created). - */ -export const getLibraryBlockTypesUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/block_types/`; - /** * Get the URL for create content in library. */ @@ -182,14 +177,6 @@ export interface CreateLibraryCollectionDataRequest { export type UpdateCollectionComponentsRequest = Partial; -/** - * Fetch the list of XBlock types that can be added to this library - */ -export async function getLibraryBlockTypes(libraryId: string): Promise { - const { data } = await getAuthenticatedHttpClient().get(getLibraryBlockTypesUrl(libraryId)); - return camelCaseObject(data); -} - /** * Fetch a content library by its ID. */ diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index 40601356b..27fad4302 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -14,7 +14,6 @@ import { type XBlockFields, type UpdateXBlockFieldsRequest, getContentLibrary, - getLibraryBlockTypes, createLibraryBlock, getContentLibraryV2List, commitLibraryChanges, @@ -59,12 +58,6 @@ export const libraryAuthoringQueryKeys = { 'list', ...(customParams ? [customParams] : []), ], - contentLibraryBlockTypes: (contentLibraryId?: string) => [ - ...libraryAuthoringQueryKeys.all, - ...libraryAuthoringQueryKeys.contentLibrary(contentLibraryId), - 'content', - 'libraryBlockTypes', - ], collection: (libraryId?: string, collectionId?: string) => [ ...libraryAuthoringQueryKeys.all, libraryId, @@ -113,16 +106,6 @@ export const useContentLibrary = (libraryId: string | undefined) => ( }) ); -/** - * Hook to fetch block types of a library. - */ -export const useLibraryBlockTypes = (libraryId: string) => ( - useQuery({ - queryKey: libraryAuthoringQueryKeys.contentLibraryBlockTypes(libraryId), - queryFn: () => getLibraryBlockTypes(libraryId), - }) -); - /** * Use this mutation to create a block in a library */ diff --git a/src/search-manager/BlockTypeLabel.test.tsx b/src/search-manager/BlockTypeLabel.test.tsx new file mode 100644 index 000000000..385278b72 --- /dev/null +++ b/src/search-manager/BlockTypeLabel.test.tsx @@ -0,0 +1,73 @@ +import { render, screen } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import BlockTypeLabel from './BlockTypeLabel'; +import messages from './messages'; + +const testCases = [ + { + blockType: 'annotatable', + count: undefined, + expectedLabel: messages['blockType.annotatable'].defaultMessage, + }, + { + blockType: 'chapter', + count: undefined, + expectedLabel: messages['blockType.chapter'].defaultMessage, + }, + { + blockType: 'chapter', + count: 10, + expectedLabel: messages['blockType.chapter'].defaultMessage, + }, + { + blockType: 'drag-and-drop-v2', + count: undefined, + expectedLabel: messages['blockType.drag-and-drop-v2'].defaultMessage, + }, + { + blockType: 'multiplechoiceresponse', + count: undefined, + expectedLabel: messages['blockType.multiplechoiceresponse'].defaultMessage, + }, + { + blockType: 'html', + count: undefined, + expectedLabel: messages['blockType.html'].defaultMessage, + }, + { + blockType: 'collection', + count: undefined, + expectedLabel: messages['blockType.collection'].defaultMessage, + }, + { + blockType: 'collection', + count: 0, + expectedLabel: messages['blockType.collection'].defaultMessage, + }, + { + blockType: 'collection', + count: 10, + expectedLabel: 'Collection (10)', + }, + // XBlock types without an explicit label are capitalized using the textTransform style + { + blockType: 'survey', + count: undefined, + expectedLabel: 'survey', + }, +]; + +describe('', () => { + test.each(testCases)( + 'render BlockTypeLabel for $blockType (count=$count)', + ({ blockType, expectedLabel, count }) => { + render( + + + , + ); + expect(screen.getByText(expectedLabel)).toBeInTheDocument(); + }, + ); +}); diff --git a/src/search-manager/BlockTypeLabel.tsx b/src/search-manager/BlockTypeLabel.tsx index 5f8a54073..f9602eb83 100644 --- a/src/search-manager/BlockTypeLabel.tsx +++ b/src/search-manager/BlockTypeLabel.tsx @@ -5,18 +5,26 @@ import messages from './messages'; /** * Displays a friendly, localized text name for the given XBlock/component type * e.g. `vertical` becomes `"Unit"` + * + * Also accepts an optional `count` number, which will be displayed if + * it's non-zero and the block label supports it. */ -const BlockTypeLabel: React.FC<{ type: string }> = ({ type }) => { - // TODO: Load the localized list of Component names from Studio REST API? - const msg = messages[`blockType.${type}`]; +const BlockTypeLabel: React.FC<{ blockType: string, count?: number }> = ({ blockType, count }) => { + const msg = messages[`blockType.${blockType}`]; + const msgWithCount = messages[`blockType.${blockType}.with_count`]; + + if (count && msgWithCount) { + return ; + } if (msg) { return ; } + // Replace underscores and hypens with spaces, then let the browser capitalize this // in a locale-aware way to get a reasonable display value. // e.g. 'drag-and-drop-v2' -> "Drag And Drop V2" - return {type.replace(/[_-]/g, ' ')}; + return {blockType.replace(/[_-]/g, ' ')}; }; export default BlockTypeLabel; diff --git a/src/search-manager/FilterByBlockType.tsx b/src/search-manager/FilterByBlockType.tsx index eba53e96e..cd887e3cc 100644 --- a/src/search-manager/FilterByBlockType.tsx +++ b/src/search-manager/FilterByBlockType.tsx @@ -108,7 +108,7 @@ const ProblemFilterItem = ({ count, handleCheckboxChange } : ProblemFilterItemPr >
- {' '} + {' '} {count}
{ Object.keys(problemTypes).length !== 0 && ( @@ -146,7 +146,7 @@ const ProblemFilterItem = ({ count, handleCheckboxChange } : ProblemFilterItemPr onChange={handleProblemCheckboxChange} >
- {' '} + {' '} {problemTypeCount}
@@ -199,7 +199,7 @@ const FilterItem = ({ blockType, count } : FilterItemProps) => { onChange={handleCheckboxChange} >
- {' '} + {' '} {count}
@@ -259,7 +259,7 @@ const FilterByBlockType: React.FC> = () => { }); const appliedFilters = [...blockTypesFilter, ...problemTypesFilter].map( - blockType => ({ label: }), + blockType => ({ label: }), ); return ( diff --git a/src/search-manager/messages.ts b/src/search-manager/messages.ts index ebda3cb91..218b452e1 100644 --- a/src/search-manager/messages.ts +++ b/src/search-manager/messages.ts @@ -56,6 +56,16 @@ const messages = defineMessages({ defaultMessage: 'Section', description: 'Name of the "Section" course outline level in Studio', }, + 'blockType.collection': { + id: 'course-authoring.course-search.blockType.collection', + defaultMessage: 'Collection', + description: 'Collection type text', + }, + 'blockType.collection.with_count': { + id: 'course-authoring.course-search.blockType.collectionWithCount', + defaultMessage: 'Collection ({count})', + description: 'Collection type text with children count', + }, 'blockType.discussion': { id: 'course-authoring.course-search.blockType.discussion', defaultMessage: 'Discussion',