diff --git a/src/editors/containers/EditorContainer/index.tsx b/src/editors/containers/EditorContainer/index.tsx index f60c72c75..670eb6ec1 100644 --- a/src/editors/containers/EditorContainer/index.tsx +++ b/src/editors/containers/EditorContainer/index.tsx @@ -18,7 +18,7 @@ import { useEditorContext } from '../../EditorContext'; import TitleHeader from './components/TitleHeader'; import * as hooks from './hooks'; import messages from './messages'; -import { parseErrorMsg } from '../../../library-authoring/add-content/AddContentContainer'; +import { parseErrorMsg } from '../../../library-authoring/add-content/AddContent'; import libraryMessages from '../../../library-authoring/add-content/messages'; import './index.scss'; diff --git a/src/generic/key-utils.test.ts b/src/generic/key-utils.test.ts index 3a1316793..8d3569d2b 100644 --- a/src/generic/key-utils.test.ts +++ b/src/generic/key-utils.test.ts @@ -4,6 +4,8 @@ import { getLibraryId, isLibraryKey, isLibraryV1Key, + getContainerTypeFromId, + ContainerType, } from './key-utils'; describe('component utils', () => { @@ -97,4 +99,16 @@ describe('component utils', () => { }); } }); + + describe('getContainerTypeFromId', () => { + for (const [input, expected] of [ + ['lct:org:lib:unit:my-unit-9284e2', ContainerType.Unit], + ['lct:OpenCraftX:ALPHA:my-unit-a3223f', undefined], + ['', undefined], + ]) { + it(`returns '${expected}' for container key '${input}'`, () => { + expect(getContainerTypeFromId(input!)).toStrictEqual(expected); + }); + } + }); }); diff --git a/src/generic/key-utils.ts b/src/generic/key-utils.ts index c1e4ad0ac..32da743da 100644 --- a/src/generic/key-utils.ts +++ b/src/generic/key-utils.ts @@ -49,3 +49,26 @@ export const buildCollectionUsageKey = (learningContextKey: string, collectionId const orgLib = learningContextKey.replace('lib:', ''); return `lib-collection:${orgLib}:${collectionId}`; }; + +export enum ContainerType { + Unit = 'unit', +} + +/** + * Given a container key like `ltc:org:lib:unit:id` + * get the container type + */ +export function getContainerTypeFromId(containerId: string): ContainerType | undefined { + const parts = containerId.split(':'); + if (parts.length < 2) { + return undefined; + } + + const maybeType = parts[parts.length - 2]; + + if (Object.values(ContainerType).includes(maybeType as ContainerType)) { + return maybeType as ContainerType; + } + + return undefined; +} diff --git a/src/library-authoring/LibraryLayout.tsx b/src/library-authoring/LibraryLayout.tsx index 21961d7b2..f6f425912 100644 --- a/src/library-authoring/LibraryLayout.tsx +++ b/src/library-authoring/LibraryLayout.tsx @@ -43,7 +43,7 @@ const LibraryLayout = () => { /** The component picker modal to use. We need to pass it as a reference instead of * directly importing it to avoid the import cycle: * ComponentPicker > LibraryAuthoringPage/LibraryCollectionPage > - * Sidebar > AddContentContainer > ComponentPicker */ + * Sidebar > AddContent > ComponentPicker */ componentPicker={ComponentPicker} > diff --git a/src/library-authoring/add-content/AddContentContainer.test.tsx b/src/library-authoring/add-content/AddContent.test.tsx similarity index 77% rename from src/library-authoring/add-content/AddContentContainer.test.tsx rename to src/library-authoring/add-content/AddContent.test.tsx index 1f1807a15..aebc7241e 100644 --- a/src/library-authoring/add-content/AddContentContainer.test.tsx +++ b/src/library-authoring/add-content/AddContent.test.tsx @@ -13,11 +13,11 @@ import { } from '../data/api.mocks'; import { getContentLibraryApiUrl, getCreateLibraryBlockUrl, getLibraryCollectionComponentApiUrl, getLibraryPasteClipboardUrl, - getXBlockFieldsApiUrl, + getXBlockFieldsApiUrl, getLibraryContainerChildrenApiUrl, } from '../data/api'; import { mockBroadcastChannel, mockClipboardEmpty, mockClipboardHtml } from '../../generic/data/api.mock'; import { LibraryProvider } from '../common/context/LibraryContext'; -import AddContentContainer from './AddContentContainer'; +import AddContent from './AddContent'; import { ComponentEditorModal } from '../components/ComponentEditorModal'; import editorCmsApi from '../../editors/data/services/cms/api'; import { ToastActionData } from '../../generic/toast-context'; @@ -32,7 +32,7 @@ jest.mock('frontend-components-tinymce-advanced-plugins', () => ({ a11ycheckerCs const { libraryId } = mockContentLibrary; const render = (collectionId?: string) => { const params: { libraryId: string, collectionId?: string } = { libraryId, collectionId }; - return baseRender(, { + return baseRender(, { path: '/library/:libraryId/:collectionId?', params, extraWrapper: ({ children }) => ( @@ -45,10 +45,25 @@ const render = (collectionId?: string) => { ), }); }; +const renderWithUnit = (unitId: string) => { + const params: { libraryId: string, unitId?: string } = { libraryId, unitId }; + return baseRender(, { + path: '/library/:libraryId/:unitId?', + params, + extraWrapper: ({ children }) => ( + + { children } + + + ), + }); +}; let axiosMock: MockAdapter; let mockShowToast: (message: string, action?: ToastActionData | undefined) => void; -describe('', () => { +describe('', () => { beforeEach(() => { const mocks = initializeMocks(); axiosMock = mocks.axiosMock; @@ -290,4 +305,71 @@ describe('', () => { expect(mockShowToast).toHaveBeenCalledWith(expectedError); }); }); + + it('should not show collection/unit buttons when create component in container', async () => { + const unitId = 'lct:orf1:lib1:unit:test-1'; + renderWithUnit(unitId); + + expect(await screen.findByRole('button', { name: 'Text' })).toBeInTheDocument(); + + expect(screen.queryByRole('button', { name: 'Collection' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Unit' })).not.toBeInTheDocument(); + }); + + it('should create a component in unit', async () => { + const unitId = 'lct:orf1:lib1:unit:test-1'; + const usageKey = mockXBlockFields.usageKeyNewHtml; + const createUrl = getCreateLibraryBlockUrl(libraryId); + const updateBlockUrl = getXBlockFieldsApiUrl(usageKey); + const linkUrl = getLibraryContainerChildrenApiUrl(unitId); + + axiosMock.onPost(createUrl).reply(200, { + id: usageKey, + }); + axiosMock.onPost(updateBlockUrl).reply(200, mockXBlockFields.dataHtml); + axiosMock.onPost(linkUrl).reply(200); + + renderWithUnit(unitId); + + const textButton = screen.getByRole('button', { name: /text/i }); + fireEvent.click(textButton); + + // Component should be linked to Unit on saving the changes in the editor. + const saveButton = screen.getByLabelText('Save changes and return to learning context'); + fireEvent.click(saveButton); + + await waitFor(() => expect(axiosMock.history.post.length).toEqual(3)); + expect(axiosMock.history.post[0].url).toEqual(createUrl); + expect(axiosMock.history.post[1].url).toEqual(updateBlockUrl); + expect(axiosMock.history.post[2].url).toEqual(linkUrl); + }); + + it('should show error on create a component in unit', async () => { + const unitId = 'lct:orf1:lib1:unit:test-1'; + const usageKey = mockXBlockFields.usageKeyNewHtml; + const createUrl = getCreateLibraryBlockUrl(libraryId); + const updateBlockUrl = getXBlockFieldsApiUrl(usageKey); + const linkUrl = getLibraryContainerChildrenApiUrl(unitId); + + axiosMock.onPost(createUrl).reply(200, { + id: usageKey, + }); + axiosMock.onPost(updateBlockUrl).reply(200, mockXBlockFields.dataHtml); + axiosMock.onPost(linkUrl).reply(400); + + renderWithUnit(unitId); + + const textButton = screen.getByRole('button', { name: /text/i }); + fireEvent.click(textButton); + + const saveButton = screen.getByLabelText('Save changes and return to learning context'); + fireEvent.click(saveButton); + + await waitFor(() => expect(axiosMock.history.post.length).toEqual(3)); + expect(axiosMock.history.post[0].url).toEqual(createUrl); + expect(axiosMock.history.post[1].url).toEqual(updateBlockUrl); + expect(axiosMock.history.post[2].url).toEqual(linkUrl); + + expect(mockShowToast).toHaveBeenCalledWith('There was an error linking the content to this container.'); + }); }); diff --git a/src/library-authoring/add-content/AddContentContainer.tsx b/src/library-authoring/add-content/AddContent.tsx similarity index 86% rename from src/library-authoring/add-content/AddContentContainer.tsx rename to src/library-authoring/add-content/AddContent.tsx index 1899318d5..e518d42e8 100644 --- a/src/library-authoring/add-content/AddContentContainer.tsx +++ b/src/library-authoring/add-content/AddContent.tsx @@ -23,6 +23,7 @@ import { useLibraryPasteClipboard, useAddComponentsToCollection, useBlockTypesMetadata, + useAddComponentsToContainer, } from '../data/apiHooks'; import { useLibraryContext } from '../common/context/LibraryContext'; import { PickLibraryContentModal } from './PickLibraryContentModal'; @@ -30,6 +31,7 @@ import { blockTypes } from '../../editors/data/constants/app'; import messages from './messages'; import type { BlockTypeMetadata } from '../data/api'; +import { getContainerTypeFromId, ContainerType } from '../../generic/key-utils'; type ContentType = { name: string, @@ -87,7 +89,12 @@ const AddContentView = ({ const { collectionId, componentPicker, + unitId, } = useLibraryContext(); + let upstreamContainerType: ContainerType | undefined; + if (unitId) { + upstreamContainerType = getContainerTypeFromId(unitId); + } const collectionButtonData = { name: intl.formatMessage(messages.collectionButton), @@ -109,21 +116,25 @@ const AddContentView = ({ return ( <> - {collectionId ? ( - componentPicker && ( - <> - - - - ) - ) : ( - + {upstreamContainerType !== ContainerType.Unit && ( + <> + {collectionId ? ( + componentPicker && ( + <> + + + + ) + ) : ( + + )} + +
+ )} - -
{/* Note: for MVP we are hiding the unuspported types, not just disabling them. */} {contentTypes.filter(ct => !ct.disabled).map((contentType) => ( { +const AddContent = () => { const intl = useIntl(); const { libraryId, @@ -194,8 +205,10 @@ const AddContentContainer = () => { openCreateCollectionModal, openCreateUnitModal, openComponentEditor, + unitId, } = useLibraryContext(); - const updateComponentsMutation = useAddComponentsToCollection(libraryId, collectionId); + const addComponentsToCollectionMutation = useAddComponentsToCollection(libraryId, collectionId); + const addComponentsToContainerMutation = useAddComponentsToContainer(libraryId, unitId); const createBlockMutation = useCreateLibraryBlock(); const pasteClipboardMutation = useLibraryPasteClipboard(); const { showToast } = useContext(ToastContext); @@ -274,9 +287,16 @@ const AddContentContainer = () => { } const linkComponent = (usageKey: string) => { - updateComponentsMutation.mutateAsync([usageKey]).catch(() => { - showToast(intl.formatMessage(messages.errorAssociateComponentMessage)); - }); + if (collectionId) { + addComponentsToCollectionMutation.mutateAsync([usageKey]).catch(() => { + showToast(intl.formatMessage(messages.errorAssociateComponentToCollectionMessage)); + }); + } + if (unitId) { + addComponentsToContainerMutation.mutateAsync([usageKey]).catch(() => { + showToast(intl.formatMessage(messages.errorAssociateComponentToContainerMessage)); + }); + } }; const onPaste = () => { @@ -374,4 +394,4 @@ const AddContentContainer = () => { ); }; -export default AddContentContainer; +export default AddContent; diff --git a/src/library-authoring/add-content/PickLibraryContentModal.tsx b/src/library-authoring/add-content/PickLibraryContentModal.tsx index e5977d240..5436bf2d7 100644 --- a/src/library-authoring/add-content/PickLibraryContentModal.tsx +++ b/src/library-authoring/add-content/PickLibraryContentModal.tsx @@ -42,7 +42,7 @@ export const PickLibraryContentModal: React.FC = ( collectionId, /** We need to get it as a reference instead of directly importing it to avoid the import cycle: * ComponentPicker > LibraryAuthoringPage/LibraryCollectionPage > - * Sidebar > AddContentContainer > ComponentPicker */ + * Sidebar > AddContent > ComponentPicker */ componentPicker: ComponentPicker, } = useLibraryContext(); @@ -65,7 +65,7 @@ export const PickLibraryContentModal: React.FC = ( showToast(intl.formatMessage(messages.successAssociateComponentMessage)); }) .catch(() => { - showToast(intl.formatMessage(messages.errorAssociateComponentMessage)); + showToast(intl.formatMessage(messages.errorAssociateComponentToCollectionMessage)); }); }, [selectedComponents]); diff --git a/src/library-authoring/add-content/index.ts b/src/library-authoring/add-content/index.ts index ae4e4ac7b..a3bd82be4 100644 --- a/src/library-authoring/add-content/index.ts +++ b/src/library-authoring/add-content/index.ts @@ -1,2 +1,2 @@ -export { default as AddContentContainer } from './AddContentContainer'; +export { default as AddContent } from './AddContent'; export { default as AddContentHeader } from './AddContentHeader'; diff --git a/src/library-authoring/add-content/messages.ts b/src/library-authoring/add-content/messages.ts index e8e6d601b..c42146aa4 100644 --- a/src/library-authoring/add-content/messages.ts +++ b/src/library-authoring/add-content/messages.ts @@ -84,11 +84,16 @@ const messages = defineMessages({ defaultMessage: 'Content linked successfully.', description: 'Message when linking of content to a collection in library is success', }, - errorAssociateComponentMessage: { + errorAssociateComponentToCollectionMessage: { id: 'course-authoring.library-authoring.associate-collection-content.error.text', defaultMessage: 'There was an error linking the content to this collection.', description: 'Message when linking of content to a collection in library fails', }, + errorAssociateComponentToContainerMessage: { + id: 'course-authoring.library-authoring.associate-container-content.error.text', + defaultMessage: 'There was an error linking the content to this container.', + description: 'Message when linking of content to a container in library fails', + }, addContentTitle: { id: 'course-authoring.library-authoring.sidebar.title.add-content', defaultMessage: 'Add Content', diff --git a/src/library-authoring/common/context/LibraryContext.tsx b/src/library-authoring/common/context/LibraryContext.tsx index f00ea4338..b415d351a 100644 --- a/src/library-authoring/common/context/LibraryContext.tsx +++ b/src/library-authoring/common/context/LibraryContext.tsx @@ -72,7 +72,7 @@ type LibraryProviderProps = { /** The component picker modal to use. We need to pass it as a reference instead of * directly importing it to avoid the import cycle: * ComponentPicker > LibraryAuthoringPage/LibraryCollectionPage > - * Sidebar > AddContentContainer > ComponentPicker */ + * Sidebar > AddContent > ComponentPicker */ componentPicker?: typeof ComponentPicker; }; diff --git a/src/library-authoring/create-unit/CreateUnitModal.tsx b/src/library-authoring/create-unit/CreateUnitModal.tsx index 30ae48546..a20f20e5d 100644 --- a/src/library-authoring/create-unit/CreateUnitModal.tsx +++ b/src/library-authoring/create-unit/CreateUnitModal.tsx @@ -13,6 +13,7 @@ import messages from './messages'; import { useCreateLibraryContainer } from '../data/apiHooks'; import { ToastContext } from '../../generic/toast-context'; import LoadingButton from '../../generic/loading-button'; +import { ContainerType } from '../../generic/key-utils'; const CreateUnitModal = () => { const intl = useIntl(); @@ -27,7 +28,7 @@ const CreateUnitModal = () => { const handleCreate = React.useCallback(async (values) => { try { await create.mutateAsync({ - containerType: 'unit', + containerType: ContainerType.Unit, ...values, }); // TODO: Navigate to the new unit diff --git a/src/library-authoring/data/api.test.ts b/src/library-authoring/data/api.test.ts index 2c69264ea..e75a2b4b1 100644 --- a/src/library-authoring/data/api.test.ts +++ b/src/library-authoring/data/api.test.ts @@ -113,6 +113,17 @@ describe('library data API', () => { axiosMock.onPost(url).reply(200); await api.restoreContainer(containerId); + }); + + it('should add components to unit', async () => { + const { axiosMock } = initializeMocks(); + const componentId = 'lb:org:lib:html:1'; + const containerId = 'ltc:org:lib:unit:1'; + const url = api.getLibraryContainerChildrenApiUrl(containerId); + + axiosMock.onPost(url).reply(200); + + await api.addComponentsToContainer(containerId, [componentId]); expect(axiosMock.history.post[0].url).toEqual(url); }); }); diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts index 1447a0091..ca8ec0aef 100644 --- a/src/library-authoring/data/api.ts +++ b/src/library-authoring/data/api.ts @@ -1,6 +1,7 @@ import { camelCaseObject, getConfig, snakeCaseObject } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { VersionSpec } from '../LibraryBlock'; +import { type ContainerType } from '../../generic/key-utils'; const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; @@ -576,7 +577,7 @@ export async function updateComponentCollections(usageKey: string, collectionKey export interface CreateLibraryContainerDataRequest { title: string; - containerType: string; + containerType: ContainerType; } /** @@ -648,3 +649,15 @@ export async function getLibraryContainerChildren(containerId: string): Promise< const { data } = await getAuthenticatedHttpClient().get(getLibraryContainerChildrenApiUrl(containerId)); return camelCaseObject(data); } + +/** + * Add components to library container + */ +export async function addComponentsToContainer(containerId: string, componentIds: string[]) { + const client = getAuthenticatedHttpClient(); + // POSTing to this URL will append children; PATCHing to it will replace the children. + await client.post( + getLibraryContainerChildrenApiUrl(containerId), + snakeCaseObject({ usageKeys: componentIds }), + ); +} diff --git a/src/library-authoring/data/apiHooks.test.tsx b/src/library-authoring/data/apiHooks.test.tsx index 540622112..f1a8b3820 100644 --- a/src/library-authoring/data/apiHooks.test.tsx +++ b/src/library-authoring/data/apiHooks.test.tsx @@ -28,6 +28,7 @@ import { useDeleteContainer, useRestoreContainer, useContainerChildren, + useAddComponentsToContainer, } from './apiHooks'; let axiosMock; @@ -258,4 +259,18 @@ describe('library api hooks', () => { ]); expect(axiosMock.history.get[0].url).toEqual(url); }); + + it('should add components to container', async () => { + const libraryId = 'lib:org:1'; + const componentId = 'lb:org:lib:html:1'; + const containerId = 'ltc:org:lib:unit:1'; + + const url = getLibraryContainerChildrenApiUrl(containerId); + + axiosMock.onPost(url).reply(200); + const { result } = renderHook(() => useAddComponentsToContainer(libraryId, containerId), { wrapper }); + await result.current.mutateAsync([componentId]); + + expect(axiosMock.history.post[0].url).toEqual(url); + }); }); diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index 65a584da1..a0a969698 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -47,6 +47,7 @@ import { restoreLibraryBlock, getBlockTypes, createLibraryContainer, + addComponentsToContainer, type CreateLibraryContainerDataRequest, getContainerMetadata, updateContainerMetadata, @@ -669,3 +670,21 @@ export const useContainerChildren = (libraryId?: string, containerId?: string) = queryFn: () => getLibraryContainerChildren(containerId!), }) ); + +/** + * Use this mutation to add components to a container + */ +export const useAddComponentsToContainer = (libraryId?: string, containerId?: string) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (componentIds: string[]) => { + if (containerId !== undefined) { + return addComponentsToContainer(containerId, componentIds); + } + return undefined; + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.containerChildren(libraryId, containerId) }); + }, + }); +}; diff --git a/src/library-authoring/library-sidebar/LibrarySidebar.tsx b/src/library-authoring/library-sidebar/LibrarySidebar.tsx index 77135e039..a4443e73e 100644 --- a/src/library-authoring/library-sidebar/LibrarySidebar.tsx +++ b/src/library-authoring/library-sidebar/LibrarySidebar.tsx @@ -7,7 +7,7 @@ import { import { Close } from '@openedx/paragon/icons'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { AddContentContainer, AddContentHeader } from '../add-content'; +import { AddContent, AddContentHeader } from '../add-content'; import { CollectionInfo, CollectionInfoHeader } from '../collections'; import { ContainerInfoHeader, UnitInfo } from '../containers'; import { SidebarBodyComponentId, useSidebarContext } from '../common/context/SidebarContext'; @@ -29,7 +29,7 @@ const LibrarySidebar = () => { const { sidebarComponentInfo, closeLibrarySidebar } = useSidebarContext(); const bodyComponentMap = { - [SidebarBodyComponentId.AddContent]: , + [SidebarBodyComponentId.AddContent]: , [SidebarBodyComponentId.Info]: , [SidebarBodyComponentId.ComponentInfo]: , [SidebarBodyComponentId.CollectionInfo]: , diff --git a/src/library-authoring/units/LibraryUnitBlocks.tsx b/src/library-authoring/units/LibraryUnitBlocks.tsx index 6bb1f06ac..6efc5cc6d 100644 --- a/src/library-authoring/units/LibraryUnitBlocks.tsx +++ b/src/library-authoring/units/LibraryUnitBlocks.tsx @@ -1,8 +1,8 @@ -import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { - ActionRow, Badge, Icon, Stack, useToggle, + ActionRow, Badge, Button, Icon, Stack, useToggle, } from '@openedx/paragon'; -import { Description } from '@openedx/paragon/icons'; +import { Add, Description } from '@openedx/paragon/icons'; import { useQueryClient } from '@tanstack/react-query'; import classNames from 'classnames'; import { useEffect, useState } from 'react'; @@ -22,8 +22,10 @@ import { libraryAuthoringQueryKeys, useContainerChildren } from '../data/apiHook import { LibraryBlock } from '../LibraryBlock'; import { useLibraryRoutes } from '../routes'; import messages from './messages'; +import { useSidebarContext } from '../common/context/SidebarContext'; export const LibraryUnitBlocks = () => { + const intl = useIntl(); const [orderedBlocks, setOrderedBlocks] = useState([]); const [isManageTagsDrawerOpen, openManageTagsDrawer, closeManageTagsDrawer] = useToggle(false); const { navigateTo } = useLibraryRoutes(); @@ -33,9 +35,14 @@ export const LibraryUnitBlocks = () => { unitId, showOnlyPublished, componentId, + readOnly, setComponentId, } = useLibraryContext(); + const { + openAddContentSidebar, + } = useSidebarContext(); + const queryClient = useQueryClient(); const { data: blocks, @@ -128,6 +135,31 @@ export const LibraryUnitBlocks = () => { {renderedBlocks} +
+
+ +
+
+ +
+
{ const { unitId, readOnly } = useLibraryContext(); const { + openAddContentSidebar, closeLibrarySidebar, openUnitInfoSidebar, sidebarComponentInfo, @@ -64,8 +65,9 @@ const HeaderActions = () => { iconBefore={Add} variant="primary rounded-0" disabled={readOnly} + onClick={openAddContentSidebar} > - {intl.formatMessage(messages.newContentButton)} + {intl.formatMessage(messages.addContentButton)} ); diff --git a/src/library-authoring/units/messages.ts b/src/library-authoring/units/messages.ts index 2ce7891ab..912efd490 100644 --- a/src/library-authoring/units/messages.ts +++ b/src/library-authoring/units/messages.ts @@ -6,9 +6,19 @@ const messages = defineMessages({ defaultMessage: 'Unit Info', description: 'Button text to unit sidebar from unit page', }, - newContentButton: { - id: 'course-authoring.library-authoring.unit-header.buttons.new-content', + addContentButton: { + id: 'course-authoring.library-authoring.unit-header.buttons.add-content', defaultMessage: 'Add Content', + description: 'Text of button to add content to unit', + }, + addExistingContentButton: { + id: 'course-authoring.library-authoring.unit-header.buttons.add-existing-content', + defaultMessage: 'Add Existing Content', + description: 'Text of button to add existing content to unit', + }, + newContentButton: { + id: 'course-authoring.library-authoring.unit-header.buttons.add-new-content', + defaultMessage: 'Add New Content', description: 'Text of button to add new content to unit', }, breadcrumbsAriaLabel: {