,
+ 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 {