diff --git a/.env b/.env index 1ace48c15..348753684 100644 --- a/.env +++ b/.env @@ -44,4 +44,6 @@ INVITE_STUDENTS_EMAIL_TO='' ENABLE_HOME_PAGE_COURSE_API_V2=true ENABLE_CHECKLIST_QUALITY='' ENABLE_GRADING_METHOD_IN_PROBLEMS=false -LIBRARY_SUPPORTED_BLOCKS="problem,video,html,drag-and-drop-v2" +# "Multi-level" blocks are unsupported in libraries +# TODO: Missing support for ORA2 +LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,openassessment" diff --git a/.env.development b/.env.development index f3e247e7c..883f0f6bf 100644 --- a/.env.development +++ b/.env.development @@ -47,4 +47,5 @@ INVITE_STUDENTS_EMAIL_TO="someone@domain.com" ENABLE_HOME_PAGE_COURSE_API_V2=true ENABLE_CHECKLIST_QUALITY=true ENABLE_GRADING_METHOD_IN_PROBLEMS=false -LIBRARY_SUPPORTED_BLOCKS="problem,video,html,drag-and-drop-v2" +# "Multi-level" blocks are unsupported in libraries +LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder" diff --git a/.env.test b/.env.test index 0be671946..378e0f17c 100644 --- a/.env.test +++ b/.env.test @@ -39,5 +39,6 @@ INVITE_STUDENTS_EMAIL_TO="someone@domain.com" ENABLE_HOME_PAGE_COURSE_API_V2=true ENABLE_CHECKLIST_QUALITY=true ENABLE_GRADING_METHOD_IN_PROBLEMS=false -# "other" is used to test the workflow for creating blocks that aren't supported by the built-in editors -LIBRARY_SUPPORTED_BLOCKS="problem,video,html,drag-and-drop-v2,other" +# "Multi-level" blocks are unsupported in libraries +# TODO: Missing support for ORA2 +LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,openassessment" diff --git a/src/index.jsx b/src/index.jsx index c885dac16..f7916b49f 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -146,7 +146,7 @@ initialize({ ENABLE_HOME_PAGE_COURSE_API_V2: process.env.ENABLE_HOME_PAGE_COURSE_API_V2 === 'true', ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true', ENABLE_GRADING_METHOD_IN_PROBLEMS: process.env.ENABLE_GRADING_METHOD_IN_PROBLEMS === 'true', - LIBRARY_SUPPORTED_BLOCKS: (process.env.LIBRARY_SUPPORTED_BLOCKS || 'problem,video,html').split(','), + LIBRARY_UNSUPPORTED_BLOCKS: (process.env.LIBRARY_UNSUPPORTED_BLOCKS || 'conditional,step-builder,problem-builder').split(','), }, 'CourseAuthoringConfig'); }, }, diff --git a/src/library-authoring/add-content/AddContentContainer.test.tsx b/src/library-authoring/add-content/AddContentContainer.test.tsx index 63b42895b..1f1807a15 100644 --- a/src/library-authoring/add-content/AddContentContainer.test.tsx +++ b/src/library-authoring/add-content/AddContentContainer.test.tsx @@ -6,7 +6,11 @@ import { waitFor, initializeMocks, } from '../../testUtils'; -import { mockContentLibrary, mockXBlockFields } from '../data/api.mocks'; +import { + mockContentLibrary, + mockBlockTypesMetadata, + mockXBlockFields, +} from '../data/api.mocks'; import { getContentLibraryApiUrl, getCreateLibraryBlockUrl, getLibraryCollectionComponentApiUrl, getLibraryPasteClipboardUrl, getXBlockFieldsApiUrl, @@ -55,7 +59,8 @@ describe('', () => { afterEach(() => { jest.restoreAllMocks(); }); - it('should render content buttons', () => { + it('should render content buttons', async () => { + mockBlockTypesMetadata.applyMock(); mockClipboardEmpty.applyMock(); render(); expect(screen.queryByRole('button', { name: /collection/i })).toBeInTheDocument(); @@ -64,8 +69,54 @@ describe('', () => { expect(screen.queryByRole('button', { name: /open reponse/i })).not.toBeInTheDocument(); // Excluded from MVP expect(screen.queryByRole('button', { name: /drag drop/i })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /video/i })).toBeInTheDocument(); - expect(screen.queryByRole('button', { name: /advanced \/ other/i })).toBeInTheDocument(); // To test not editor supported blocks expect(screen.queryByRole('button', { name: /copy from clipboard/i })).not.toBeInTheDocument(); + expect(await screen.findByRole('button', { name: /advanced \/ other/i })).toBeInTheDocument(); + }); + + it('should render advanced content buttons', async () => { + mockBlockTypesMetadata.applyMock(); + mockClipboardEmpty.applyMock(); + render(); + + const advancedButton = await screen.findByRole('button', { name: /advanced \/ other/i }); + fireEvent.click(advancedButton); + + expect(await screen.findByRole('button', { name: /poll/i })).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: /survey/i })).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: /google document/i })).toBeInTheDocument(); + }); + + it('should return to content view fron advanced block creation view', async () => { + mockBlockTypesMetadata.applyMock(); + mockClipboardEmpty.applyMock(); + render(); + + const advancedButton = await screen.findByRole('button', { name: /advanced \/ other/i }); + fireEvent.click(advancedButton); + + expect(await screen.findByRole('button', { name: /poll/i })).toBeInTheDocument(); + + const returnButton = screen.getByRole('button', { name: /back to list/i }); + fireEvent.click(returnButton); + + expect(screen.queryByRole('button', { name: /text/i })).toBeInTheDocument(); + }); + + it('should create an advanced content', async () => { + mockBlockTypesMetadata.applyMock(); + mockClipboardEmpty.applyMock(); + const url = getCreateLibraryBlockUrl(libraryId); + axiosMock.onPost(url).reply(200); + render(); + + const advancedButton = await screen.findByRole('button', { name: /advanced \/ other/i }); + fireEvent.click(advancedButton); + + const surveyButton = await screen.findByRole('button', { name: /survey/i }); + fireEvent.click(surveyButton); + + await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(url)); + await waitFor(() => expect(axiosMock.history.patch.length).toEqual(0)); }); it('should open the editor modal to create a content when the block is supported', async () => { @@ -80,40 +131,6 @@ describe('', () => { expect(await screen.findByRole('heading', { name: /Text/ })).toBeInTheDocument(); }); - it('should create a component when the block is not supported by the editor', async () => { - mockClipboardEmpty.applyMock(); - const url = getCreateLibraryBlockUrl(libraryId); - axiosMock.onPost(url).reply(200); - render(); - const textButton = screen.getByRole('button', { name: /other/i }); - fireEvent.click(textButton); - await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(url)); - await waitFor(() => expect(axiosMock.history.patch.length).toEqual(0)); - }); - - it('should create a content in a collection for non-editable blocks', async () => { - mockClipboardEmpty.applyMock(); - const collectionId = 'some-collection-id'; - const url = getCreateLibraryBlockUrl(libraryId); - const collectionComponentUrl = getLibraryCollectionComponentApiUrl( - libraryId, - collectionId, - ); - - axiosMock.onPost(url).reply(200, { id: 'some-component-id' }); - axiosMock.onPatch(collectionComponentUrl).reply(200); - - render(collectionId); - - // Select a block that is not supported by the editor should create the component - const textButton = screen.getByRole('button', { name: /other/i }); - fireEvent.click(textButton); - - await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(url)); - await waitFor(() => expect(axiosMock.history.patch.length).toEqual(1)); - await waitFor(() => expect(axiosMock.history.patch[0].url).toEqual(collectionComponentUrl)); - }); - it('should create a content in a collection for editable blocks', async () => { mockClipboardEmpty.applyMock(); const collectionId = 'some-collection-id'; @@ -225,7 +242,7 @@ describe('', () => { it('should stop user from pasting unsupported blocks and show toast', async () => { // Simulate having an HTML block in the clipboard: - mockClipboardHtml.applyMock('openassessment'); + mockClipboardHtml.applyMock('conditional'); const errMsg = 'Libraries do not support this type of content yet.'; diff --git a/src/library-authoring/add-content/AddContentContainer.tsx b/src/library-authoring/add-content/AddContentContainer.tsx index d47f33e7e..f2d42fc12 100644 --- a/src/library-authoring/add-content/AddContentContainer.tsx +++ b/src/library-authoring/add-content/AddContentContainer.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useMemo } from 'react'; import type { MessageDescriptor } from 'react-intl'; import { useSelector } from 'react-redux'; import { @@ -18,18 +18,25 @@ import { Question, VideoCamera, ContentPaste, + KeyboardBackspace, } from '@openedx/paragon/icons'; import { v4 as uuid4 } from 'uuid'; import { ToastContext } from '../../generic/toast-context'; import { useCopyToClipboard } from '../../generic/clipboard'; import { getCanEdit } from '../../course-unit/data/selectors'; -import { useCreateLibraryBlock, useLibraryPasteClipboard, useAddComponentsToCollection } from '../data/apiHooks'; +import { + useCreateLibraryBlock, + useLibraryPasteClipboard, + useAddComponentsToCollection, + useBlockTypesMetadata, +} from '../data/apiHooks'; import { useLibraryContext } from '../common/context/LibraryContext'; import { PickLibraryContentModal } from './PickLibraryContentModal'; import { blockTypes } from '../../editors/data/constants/app'; import messages from './messages'; +import type { BlockTypeMetadata } from '../data/api'; type ContentType = { name: string, @@ -43,6 +50,20 @@ type AddContentButtonProps = { onCreateContent: (blockType: string) => void, }; +type AddContentViewProps = { + contentTypes: ContentType[], + onCreateContent: (blockType: string) => void, + isAddLibraryContentModalOpen: boolean, + closeAddLibraryContentModal: () => void, +}; + +type AddAdvancedContentViewProps = { + closeAdvancedList: () => void, + onCreateContent: (blockType: string) => void, + advancedBlocks: Record, + isBlockTypeEnabled: (blockType) => boolean, +}; + const AddContentButton = ({ contentType, onCreateContent } : AddContentButtonProps) => { const { name, @@ -62,6 +83,93 @@ const AddContentButton = ({ contentType, onCreateContent } : AddContentButtonPro ); }; + +const AddContentView = ({ + contentTypes, + onCreateContent, + isAddLibraryContentModalOpen, + closeAddLibraryContentModal, +}: AddContentViewProps) => { + const intl = useIntl(); + const { + collectionId, + componentPicker, + } = useLibraryContext(); + + const collectionButtonData = { + name: intl.formatMessage(messages.collectionButton), + disabled: false, + icon: BookOpen, + blockType: 'collection', + }; + + const libraryContentButtonData = { + name: intl.formatMessage(messages.libraryContentButton), + disabled: false, + icon: Folder, + blockType: 'libraryContent', + }; + + return ( + <> + {collectionId ? ( + componentPicker && ( + <> + + + + ) + ) : ( + + )} +
+ {/* Note: for MVP we are hiding the unuspported types, not just disabling them. */} + {contentTypes.filter(ct => !ct.disabled).map((contentType) => ( + + ))} + + ); +}; + +const AddAdvancedContentView = ({ + closeAdvancedList, + onCreateContent, + advancedBlocks, + isBlockTypeEnabled, +}: AddAdvancedContentViewProps) => { + const intl = useIntl(); + return ( + <> +
+ +
+ {Object.keys(advancedBlocks).map((blockType) => ( + isBlockTypeEnabled(blockType) ? ( + + ) : null + ))} + + ); +}; + export const parseErrorMsg = ( intl, error: any, @@ -79,6 +187,7 @@ export const parseErrorMsg = ( } return intl.formatMessage(defaultMessage); }; + const AddContentContainer = () => { const intl = useIntl(); const { @@ -86,32 +195,27 @@ const AddContentContainer = () => { collectionId, openCreateCollectionModal, openComponentEditor, - componentPicker, } = useLibraryContext(); const updateComponentsMutation = useAddComponentsToCollection(libraryId, collectionId); const createBlockMutation = useCreateLibraryBlock(); const pasteClipboardMutation = useLibraryPasteClipboard(); const { showToast } = useContext(ToastContext); const canEdit = useSelector(getCanEdit); - const { showPasteXBlock, sharedClipboardData } = useCopyToClipboard(canEdit); + const { sharedClipboardData } = useCopyToClipboard(canEdit); + const { showPasteXBlock } = useCopyToClipboard(canEdit); const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle(); + const [isAdvancedListOpen, showAdvancedList, closeAdvancedList] = useToggle(); - const isBlockTypeEnabled = (blockType: string) => getConfig().LIBRARY_SUPPORTED_BLOCKS.includes(blockType); + // We use block types data from backend to verify the enabled basic and advanced blocks. + // Also, we use that data to get the translated display name of the block. + const { data: blockTypesDataList } = useBlockTypesMetadata(libraryId); + const blockTypesData = useMemo(() => blockTypesDataList?.reduce((acc, block) => { + acc[block.blockType] = block; + return acc; + }, {}), [blockTypesDataList]); - const collectionButtonData = { - name: intl.formatMessage(messages.collectionButton), - disabled: false, - icon: BookOpen, - blockType: 'collection', - }; - - const libraryContentButtonData = { - name: intl.formatMessage(messages.libraryContentButton), - disabled: false, - icon: Folder, - blockType: 'libraryContent', - }; + const isBlockTypeEnabled = (blockType: string) => !getConfig().LIBRARY_UNSUPPORTED_BLOCKS.includes(blockType); const contentTypes = [ { @@ -144,14 +248,27 @@ const AddContentContainer = () => { icon: VideoCamera, blockType: 'video', }, - { - name: intl.formatMessage(messages.otherTypeButton), - disabled: !isBlockTypeEnabled('other'), - icon: AutoAwesome, - blockType: 'other', // This block doesn't exist yet. - }, ]; + const isBasicBlock = (blockType: string) => contentTypes.some( + content => content.blockType === blockType, + ); + + const advancedBlocks = useMemo(() => (blockTypesData ? Object.fromEntries( + Object.entries(blockTypesData).filter(([key]) => !isBasicBlock(key)), + ) : {}), [blockTypesData]) as Record; + + // Include the 'Advanced / Other' button if there are enabled advanced Xblocks + if (Object.keys(advancedBlocks).length > 0) { + const pasteButton = { + name: intl.formatMessage(messages.otherTypeButton), + disabled: false, + icon: AutoAwesome, + blockType: 'advancedXBlock', + }; + contentTypes.push(pasteButton); + } + // Include the 'Paste from Clipboard' button if there is an Xblock in the clipboard // that can be pasted if (showPasteXBlock) { @@ -222,6 +339,8 @@ const AddContentContainer = () => { openCreateCollectionModal(); } else if (blockType === 'libraryContent') { showAddLibraryContentModal(); + } else if (blockType === 'advancedXBlock') { + showAdvancedList(); } else { onCreateBlock(blockType); } @@ -234,36 +353,21 @@ const AddContentContainer = () => { return ( - {collectionId ? ( - componentPicker && ( - <> - - - - ) - ) : ( - + ) : ( + )} -
- {/* Note: for MVP we are hiding the unuspported types, not just disabling them. */} - {contentTypes - .filter((ct) => !ct.disabled) - .map((contentType) => ( - - ))}
); }; diff --git a/src/library-authoring/add-content/messages.ts b/src/library-authoring/add-content/messages.ts index 898073eb4..dc0f33eff 100644 --- a/src/library-authoring/add-content/messages.ts +++ b/src/library-authoring/add-content/messages.ts @@ -117,6 +117,11 @@ const messages = defineMessages({ defaultMessage: 'Libraries do not support this type of content yet.', description: 'Message when unsupported block is pasted in library', }, + backToAddContentListButton: { + id: 'course-authoring.library-authoring.add-content.buttons.back', + defaultMessage: 'Back to List', + description: 'Messag of button in advanced creation view to return to the main creation view.', + }, }); export default messages; diff --git a/src/library-authoring/common/context/LibraryContext.tsx b/src/library-authoring/common/context/LibraryContext.tsx index 2e5e9526e..061a1326d 100644 --- a/src/library-authoring/common/context/LibraryContext.tsx +++ b/src/library-authoring/common/context/LibraryContext.tsx @@ -9,7 +9,7 @@ import { import { useParams } from 'react-router-dom'; import type { ComponentPicker } from '../../component-picker'; -import type { ContentLibrary } from '../../data/api'; +import type { ContentLibrary, BlockTypeMetadata } from '../../data/api'; import { useContentLibrary } from '../../data/apiHooks'; import { useComponentPickerContext } from './ComponentPickerContext'; @@ -42,6 +42,7 @@ export type LibraryContextData = { openComponentEditor: (usageKey: string, onClose?: (data?:any) => void, blockType?:string) => void; closeComponentEditor: (data?:any) => void; componentPicker?: typeof ComponentPicker; + blockTypesData?: Record; }; /** diff --git a/src/library-authoring/component-info/ComponentInfo.test.tsx b/src/library-authoring/component-info/ComponentInfo.test.tsx index 6ccebc29f..f60d30398 100644 --- a/src/library-authoring/component-info/ComponentInfo.test.tsx +++ b/src/library-authoring/component-info/ComponentInfo.test.tsx @@ -43,7 +43,7 @@ describe(' Sidebar', () => { initializeMocks(); render( , - withLibraryId(mockContentLibrary.libraryId, mockLibraryBlockMetadata.usageKeyThirdPartyXBlock), + withLibraryId(mockContentLibrary.libraryId, mockLibraryBlockMetadata.usageKeyUnsupportedXBlock), ); const editButton = await screen.findByRole('button', { name: /Edit component/ }); diff --git a/src/library-authoring/components/ComponentEditorModal.tsx b/src/library-authoring/components/ComponentEditorModal.tsx index 0938fa7cc..74ffc8538 100644 --- a/src/library-authoring/components/ComponentEditorModal.tsx +++ b/src/library-authoring/components/ComponentEditorModal.tsx @@ -15,7 +15,7 @@ export function canEditComponent(usageKey: string): boolean { return false; } - return getConfig().LIBRARY_SUPPORTED_BLOCKS.includes(blockType); + return !getConfig().LIBRARY_UNSUPPORTED_BLOCKS.includes(blockType); } export const ComponentEditorModal: React.FC> = () => { diff --git a/src/library-authoring/data/api.mocks.ts b/src/library-authoring/data/api.mocks.ts index 490f021f8..0eda4a83f 100644 --- a/src/library-authoring/data/api.mocks.ts +++ b/src/library-authoring/data/api.mocks.ts @@ -327,7 +327,7 @@ export async function mockLibraryBlockMetadata(usageKey: string): Promise jest.spyOn(api, 'getLibraryTeam').mockImplementation(mockGetLibraryTeam); +/** + * Mock for `getBlockTypes()` + * + * Use `mockBlockTypesMetadata.applyMock()` to apply it to the whole test suite. + */ +export async function mockBlockTypesMetadata(libraryId: string): Promise { + const thisMock = mockBlockTypesMetadata; + switch (libraryId) { + case mockContentLibrary.libraryId: return thisMock.blockTypesMetadata; + default: { + return []; + } + } +} + +mockBlockTypesMetadata.blockTypesMetadata = [ + { blockType: 'poll', displayName: 'Poll' }, + { blockType: 'survey', displayName: 'Survey' }, + { blockType: 'google-document', displayName: 'Google Document' }, +]; +/** Apply this mock. Returns a spy object that can tell you if it's been called. */ +mockBlockTypesMetadata.applyMock = () => jest.spyOn(api, 'getBlockTypes').mockImplementation(mockBlockTypesMetadata); + export async function mockComponentDownstreamLinks( usageKey: string, ): ReturnType { diff --git a/src/library-authoring/data/api.test.ts b/src/library-authoring/data/api.test.ts index 86a524990..19cffcfeb 100644 --- a/src/library-authoring/data/api.test.ts +++ b/src/library-authoring/data/api.test.ts @@ -66,6 +66,19 @@ describe('library data API', () => { }); }); + describe('getBlockTypes', () => { + it('should get block types metadata', async () => { + const { axiosMock } = initializeMocks(); + const libraryId = 'lib:org:1'; + const url = api.getBlockTypesMetaDataUrl(libraryId); + axiosMock.onGet(url).reply(200); + + await api.getBlockTypes(libraryId); + + expect(axiosMock.history.get[0].url).toEqual(url); + }); + }); + it('should create collection', async () => { const { axiosMock } = initializeMocks(); const libraryId = 'lib:org:1'; diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts index 2f4251d28..c9d9917a4 100644 --- a/src/library-authoring/data/api.ts +++ b/src/library-authoring/data/api.ts @@ -24,6 +24,11 @@ export const getLibraryTeamApiUrl = (libraryId: string) => `${getApiBaseUrl()}/a */ export const getLibraryTeamMemberApiUrl = (libraryId: string, username: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/team/user/${username}/`; +/** + * Get the URL for block types metadata. + */ +export const getBlockTypesMetaDataUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/block_types/`; + /** * Get the URL for library block metadata. */ @@ -263,6 +268,11 @@ export interface CreateLibraryCollectionDataRequest { description: string | null; } +export interface BlockTypeMetadata { + blockType: string; + displayName: string; +} + export type UpdateCollectionComponentsRequest = Partial; /** @@ -384,6 +394,16 @@ export async function updateLibraryTeamMember(memberData: UpdateLibraryTeamMembe return camelCaseObject(data); } +/** + * Get the list of XBlock types that can be added to this library + */ +export async function getBlockTypes(libraryId: string): Promise { + const client = getAuthenticatedHttpClient(); + const url = getBlockTypesMetaDataUrl(libraryId); + const { data } = await client.get(url); + return camelCaseObject(data); +} + /** * Paste clipboard content into library. */ diff --git a/src/library-authoring/data/apiHooks.test.tsx b/src/library-authoring/data/apiHooks.test.tsx index 62fadf31e..4cf659fb5 100644 --- a/src/library-authoring/data/apiHooks.test.tsx +++ b/src/library-authoring/data/apiHooks.test.tsx @@ -12,6 +12,7 @@ import { getLibraryCollectionComponentApiUrl, getLibraryCollectionsApiUrl, getLibraryCollectionApiUrl, + getBlockTypesMetaDataUrl, } from './api'; import { useCommitLibraryChanges, @@ -20,6 +21,7 @@ import { useRevertLibraryChanges, useAddComponentsToCollection, useCollection, + useBlockTypesMetadata, } from './apiHooks'; let axiosMock; @@ -123,4 +125,17 @@ describe('library api hooks', () => { expect(result.current.data).toEqual({ testData: 'test-value' }); expect(axiosMock.history.get[0].url).toEqual(url); }); + + it('should get block types metadata', async () => { + const libraryId = 'lib:org:1'; + const url = getBlockTypesMetaDataUrl(libraryId); + + axiosMock.onGet(url).reply(200, { 'test-data': 'test-value' }); + const { result } = renderHook(() => useBlockTypesMetadata(libraryId), { wrapper }); + await waitFor(() => { + expect(result.current.isLoading).toBeFalsy(); + }); + expect(result.current.data).toEqual({ testData: 'test-value' }); + expect(axiosMock.history.get[0].url).toEqual(url); + }); }); diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index a707e4681..274a89e02 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -45,6 +45,7 @@ import { publishXBlock, deleteXBlockAsset, restoreLibraryBlock, + getBlockTypes, getComponentDownstreamLinks, } from './api'; import { VersionSpec } from '../LibraryBlock'; @@ -85,6 +86,11 @@ export const libraryAuthoringQueryKeys = { libraryId, collectionId, ], + blockTypes: (libraryId?: string) => [ + ...libraryAuthoringQueryKeys.all, + 'blockTypes', + libraryId, + ], }; export const xblockQueryKeys = { @@ -248,6 +254,17 @@ export const useLibraryTeam = (libraryId: string | undefined) => ( }) ); +/** + * Hook to fetch the list of XBlock types that can be added to this library. + */ +export const useBlockTypesMetadata = (libraryId: string | undefined) => ( + useQuery({ + queryKey: libraryAuthoringQueryKeys.blockTypes(libraryId), + queryFn: () => getBlockTypes(libraryId!), + enabled: libraryId !== undefined, + }) +); + /** * Hook to add a new member to a content library's team */ diff --git a/src/setupTest.js b/src/setupTest.js index 776da0c0b..923399a41 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -48,7 +48,7 @@ mergeConfig({ ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true', STUDIO_BASE_URL: process.env.STUDIO_BASE_URL || null, LMS_BASE_URL: process.env.LMS_BASE_URL || null, - LIBRARY_SUPPORTED_BLOCKS: (process.env.LIBRARY_SUPPORTED_BLOCKS || 'problem,video,html').split(','), + LIBRARY_UNSUPPORTED_BLOCKS: (process.env.LIBRARY_UNSUPPORTED_BLOCKS || 'conditional,step-builder,problem-builder').split(','), }, 'CourseAuthoringConfig'); class ResizeObserver {