feat: Add component to Unit [FC-0083] (#1784)
Creation workflow in unit page.
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<SidebarProvider>
|
||||
|
||||
@@ -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(<AddContentContainer />, {
|
||||
return baseRender(<AddContent />, {
|
||||
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(<AddContent />, {
|
||||
path: '/library/:libraryId/:unitId?',
|
||||
params,
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider
|
||||
libraryId={libraryId}
|
||||
>
|
||||
{ children }
|
||||
<ComponentEditorModal />
|
||||
</LibraryProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
let axiosMock: MockAdapter;
|
||||
let mockShowToast: (message: string, action?: ToastActionData | undefined) => void;
|
||||
|
||||
describe('<AddContentContainer />', () => {
|
||||
describe('<AddContent />', () => {
|
||||
beforeEach(() => {
|
||||
const mocks = initializeMocks();
|
||||
axiosMock = mocks.axiosMock;
|
||||
@@ -290,4 +305,71 @@ describe('<AddContentContainer />', () => {
|
||||
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.');
|
||||
});
|
||||
});
|
||||
@@ -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 && (
|
||||
<>
|
||||
<AddContentButton contentType={libraryContentButtonData} onCreateContent={onCreateContent} />
|
||||
<PickLibraryContentModal
|
||||
isOpen={isAddLibraryContentModalOpen}
|
||||
onClose={closeAddLibraryContentModal}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<AddContentButton contentType={collectionButtonData} onCreateContent={onCreateContent} />
|
||||
{upstreamContainerType !== ContainerType.Unit && (
|
||||
<>
|
||||
{collectionId ? (
|
||||
componentPicker && (
|
||||
<>
|
||||
<AddContentButton contentType={libraryContentButtonData} onCreateContent={onCreateContent} />
|
||||
<PickLibraryContentModal
|
||||
isOpen={isAddLibraryContentModalOpen}
|
||||
onClose={closeAddLibraryContentModal}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<AddContentButton contentType={collectionButtonData} onCreateContent={onCreateContent} />
|
||||
)}
|
||||
<AddContentButton contentType={unitButtonData} onCreateContent={onCreateContent} />
|
||||
<hr className="w-100 bg-gray-500" />
|
||||
</>
|
||||
)}
|
||||
<AddContentButton contentType={unitButtonData} onCreateContent={onCreateContent} />
|
||||
<hr className="w-100 bg-gray-500" />
|
||||
{/* Note: for MVP we are hiding the unuspported types, not just disabling them. */}
|
||||
{contentTypes.filter(ct => !ct.disabled).map((contentType) => (
|
||||
<AddContentButton
|
||||
@@ -186,7 +197,7 @@ export const parseErrorMsg = (
|
||||
return intl.formatMessage(defaultMessage);
|
||||
};
|
||||
|
||||
const AddContentContainer = () => {
|
||||
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;
|
||||
@@ -42,7 +42,7 @@ export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = (
|
||||
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<PickLibraryContentModalProps> = (
|
||||
showToast(intl.formatMessage(messages.successAssociateComponentMessage));
|
||||
})
|
||||
.catch(() => {
|
||||
showToast(intl.formatMessage(messages.errorAssociateComponentMessage));
|
||||
showToast(intl.formatMessage(messages.errorAssociateComponentToCollectionMessage));
|
||||
});
|
||||
}, [selectedComponents]);
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { default as AddContentContainer } from './AddContentContainer';
|
||||
export { default as AddContent } from './AddContent';
|
||||
export { default as AddContentHeader } from './AddContentHeader';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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]: <AddContentContainer />,
|
||||
[SidebarBodyComponentId.AddContent]: <AddContent />,
|
||||
[SidebarBodyComponentId.Info]: <LibraryInfo />,
|
||||
[SidebarBodyComponentId.ComponentInfo]: <ComponentInfo />,
|
||||
[SidebarBodyComponentId.CollectionInfo]: <CollectionInfo />,
|
||||
|
||||
@@ -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<LibraryBlockMetadata[]>([]);
|
||||
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 = () => {
|
||||
<DraggableList itemList={orderedBlocks} setState={setOrderedBlocks} updateOrder={handleReorder}>
|
||||
{renderedBlocks}
|
||||
</DraggableList>
|
||||
<div className="d-flex">
|
||||
<div className="w-100 mr-2">
|
||||
<Button
|
||||
className="ml-2"
|
||||
iconBefore={Add}
|
||||
variant="outline-primary rounded-0"
|
||||
disabled={readOnly}
|
||||
onClick={openAddContentSidebar}
|
||||
block
|
||||
>
|
||||
{intl.formatMessage(messages.newContentButton)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-100 ml-2">
|
||||
<Button
|
||||
className="ml-2"
|
||||
iconBefore={Add}
|
||||
variant="outline-primary rounded-0"
|
||||
disabled
|
||||
block
|
||||
>
|
||||
{intl.formatMessage(messages.addExistingContentButton)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ContentTagsDrawerSheet
|
||||
id={componentId}
|
||||
onClose={onTagSidebarClose}
|
||||
|
||||
@@ -26,6 +26,7 @@ const HeaderActions = () => {
|
||||
|
||||
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)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user