feat: add an allowlist of for supported blocks in library [FC-0062] (#1378)
* feat: show error msg from server on paste * feat: add an allowlist of for supported blocks in library Libraries v2 currently don't support editing blocks other than problem, text and videos. This commit adds a configuration variable called `LIBRARY_SUPPORTED_BLOCKS` to setup allowed list of block types users can paste into libraries. By default it is set to support 'problem,text,video,html`. * feat: enable add button for blocks based on setting --------- Co-authored-by: Rômulo Penido <romulo@opencraft.com>
This commit is contained in:
1
.env
1
.env
@@ -45,3 +45,4 @@ ENABLE_HOME_PAGE_COURSE_API_V2=false
|
||||
ENABLE_CHECKLIST_QUALITY=''
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
|
||||
LIBRARY_MODE="v1 only"
|
||||
LIBRARY_SUPPORTED_BLOCKS="problem,video,html"
|
||||
|
||||
@@ -48,3 +48,4 @@ ENABLE_HOME_PAGE_COURSE_API_V2=false
|
||||
ENABLE_CHECKLIST_QUALITY=true
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
|
||||
LIBRARY_MODE="mixed"
|
||||
LIBRARY_SUPPORTED_BLOCKS="problem,video,html"
|
||||
|
||||
@@ -40,3 +40,4 @@ ENABLE_HOME_PAGE_COURSE_API_V2=true
|
||||
ENABLE_CHECKLIST_QUALITY=true
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
|
||||
LIBRARY_MODE="mixed"
|
||||
LIBRARY_SUPPORTED_BLOCKS="problem,video,html"
|
||||
|
||||
@@ -18,7 +18,7 @@ mockClipboardEmpty.applyMockOnce = () => jest.spyOn(api, 'getClipboard').mockImp
|
||||
/**
|
||||
* Mock for `getClipboard()` that simulates a copied HTML component
|
||||
*/
|
||||
export async function mockClipboardHtml(): Promise<api.ClipboardStatus> {
|
||||
export async function mockClipboardHtml(blockType?: string): Promise<api.ClipboardStatus> {
|
||||
return {
|
||||
content: {
|
||||
id: 69,
|
||||
@@ -26,7 +26,7 @@ export async function mockClipboardHtml(): Promise<api.ClipboardStatus> {
|
||||
created: '2024-01-16T13:33:21.314439Z',
|
||||
purpose: 'clipboard',
|
||||
status: 'ready',
|
||||
blockType: 'html',
|
||||
blockType: blockType || 'html',
|
||||
blockTypeDisplay: 'Text',
|
||||
olxUrl: 'http://localhost:18010/api/content-staging/v1/staged-content/69/olx',
|
||||
displayName: 'Blank HTML Page',
|
||||
@@ -36,7 +36,7 @@ export async function mockClipboardHtml(): Promise<api.ClipboardStatus> {
|
||||
sourceEditUrl: 'http://localhost:18010/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical1',
|
||||
};
|
||||
}
|
||||
mockClipboardHtml.applyMock = () => jest.spyOn(api, 'getClipboard').mockImplementation(mockClipboardHtml);
|
||||
mockClipboardHtml.applyMock = (blockType?: string) => jest.spyOn(api, 'getClipboard').mockImplementation(() => mockClipboardHtml(blockType));
|
||||
mockClipboardHtml.applyMockOnce = () => jest.spyOn(api, 'getClipboard').mockImplementationOnce(mockClipboardHtml);
|
||||
|
||||
/** Mock the DOM `BroadcastChannel` API which the clipboard code uses */
|
||||
|
||||
@@ -133,6 +133,7 @@ initialize({
|
||||
ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true',
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS: process.env.ENABLE_GRADING_METHOD_IN_PROBLEMS === 'true',
|
||||
LIBRARY_MODE: process.env.LIBRARY_MODE || 'v1 only',
|
||||
LIBRARY_SUPPORTED_BLOCKS: (process.env.LIBRARY_SUPPORTED_BLOCKS || 'problem,video,html').split(','),
|
||||
}, 'CourseAuthoringConfig');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('<AddContentContainer />', () => {
|
||||
});
|
||||
|
||||
it('should handle failure to paste content', async () => {
|
||||
const { axiosMock } = initializeMocks();
|
||||
const { axiosMock, mockShowToast } = initializeMocks();
|
||||
// Simulate having an HTML block in the clipboard:
|
||||
mockClipboardHtml.applyMock();
|
||||
|
||||
@@ -89,8 +89,54 @@ describe('<AddContentContainer />', () => {
|
||||
const pasteButton = await screen.findByRole('button', { name: /paste from clipboard/i });
|
||||
fireEvent.click(pasteButton);
|
||||
|
||||
await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(pasteUrl));
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.post[0].url).toEqual(pasteUrl);
|
||||
expect(mockShowToast).toHaveBeenCalledWith('There was an error pasting the content.');
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: check that an actual error message is shown?!
|
||||
it('should handle failure to paste content and show server error if available', async () => {
|
||||
const { axiosMock, mockShowToast } = initializeMocks();
|
||||
// Simulate having an HTML block in the clipboard:
|
||||
mockClipboardHtml.applyMock();
|
||||
|
||||
const errMsg = 'Libraries do not support this type of content yet.';
|
||||
const pasteUrl = getLibraryPasteClipboardUrl(libraryId);
|
||||
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
axiosMock.onPost(pasteUrl).reply(() => Promise.reject({
|
||||
customAttributes: {
|
||||
httpErrorStatus: 400,
|
||||
httpErrorResponseData: JSON.stringify({ block_type: errMsg }),
|
||||
},
|
||||
}));
|
||||
|
||||
render();
|
||||
|
||||
const pasteButton = await screen.findByRole('button', { name: /paste from clipboard/i });
|
||||
fireEvent.click(pasteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.post[0].url).toEqual(pasteUrl);
|
||||
expect(mockShowToast).toHaveBeenCalledWith(errMsg);
|
||||
});
|
||||
});
|
||||
|
||||
it('should stop user from pasting unsupported blocks and show toast', async () => {
|
||||
const { axiosMock, mockShowToast } = initializeMocks();
|
||||
// Simulate having an HTML block in the clipboard:
|
||||
mockClipboardHtml.applyMock('openassessment');
|
||||
|
||||
const errMsg = 'Libraries do not support this type of content yet.';
|
||||
|
||||
render();
|
||||
|
||||
const pasteButton = await screen.findByRole('button', { name: /paste from clipboard/i });
|
||||
fireEvent.click(pasteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.post.length).toEqual(0);
|
||||
expect(mockShowToast).toHaveBeenCalledWith(errMsg);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Button,
|
||||
} from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
Article,
|
||||
AutoAwesome,
|
||||
@@ -61,17 +62,31 @@ const AddContentButton = ({ contentType, onCreateContent } : AddContentButtonPro
|
||||
|
||||
const AddContentContainer = () => {
|
||||
const intl = useIntl();
|
||||
const { libraryId, collectionId } = useParams();
|
||||
const { collectionId } = useParams();
|
||||
const {
|
||||
libraryId,
|
||||
openCreateCollectionModal,
|
||||
openComponentEditor,
|
||||
} = useLibraryContext();
|
||||
const createBlockMutation = useCreateLibraryBlock();
|
||||
const updateComponentsMutation = useUpdateCollectionComponents(libraryId, collectionId);
|
||||
const pasteClipboardMutation = useLibraryPasteClipboard();
|
||||
const { showToast } = useContext(ToastContext);
|
||||
const canEdit = useSelector(getCanEdit);
|
||||
const { showPasteXBlock } = useCopyToClipboard(canEdit);
|
||||
const {
|
||||
openCreateCollectionModal,
|
||||
openComponentEditor,
|
||||
} = useLibraryContext();
|
||||
const { showPasteXBlock, sharedClipboardData } = useCopyToClipboard(canEdit);
|
||||
|
||||
const parsePasteErrorMsg = (error: any) => {
|
||||
let errMsg: string;
|
||||
try {
|
||||
const { customAttributes: { httpErrorResponseData } } = error;
|
||||
errMsg = JSON.parse(httpErrorResponseData).block_type;
|
||||
} catch (_err) {
|
||||
errMsg = intl.formatMessage(messages.errorPasteClipboardMessage);
|
||||
}
|
||||
return errMsg;
|
||||
};
|
||||
|
||||
const isBlockTypeEnabled = (blockType: string) => getConfig().LIBRARY_SUPPORTED_BLOCKS.includes(blockType);
|
||||
|
||||
const collectionButtonData = {
|
||||
name: intl.formatMessage(messages.collectionButton),
|
||||
@@ -82,37 +97,37 @@ const AddContentContainer = () => {
|
||||
const contentTypes = [
|
||||
{
|
||||
name: intl.formatMessage(messages.textTypeButton),
|
||||
disabled: false,
|
||||
disabled: !isBlockTypeEnabled('html'),
|
||||
icon: Article,
|
||||
blockType: 'html',
|
||||
},
|
||||
{
|
||||
name: intl.formatMessage(messages.problemTypeButton),
|
||||
disabled: false,
|
||||
disabled: !isBlockTypeEnabled('problem'),
|
||||
icon: Question,
|
||||
blockType: 'problem',
|
||||
},
|
||||
{
|
||||
name: intl.formatMessage(messages.openResponseTypeButton),
|
||||
disabled: false,
|
||||
disabled: !isBlockTypeEnabled('openassessment'),
|
||||
icon: Create,
|
||||
blockType: 'openassessment',
|
||||
},
|
||||
{
|
||||
name: intl.formatMessage(messages.dragDropTypeButton),
|
||||
disabled: false,
|
||||
disabled: !isBlockTypeEnabled('drag-and-drop-v2'),
|
||||
icon: ThumbUpOutline,
|
||||
blockType: 'drag-and-drop-v2',
|
||||
},
|
||||
{
|
||||
name: intl.formatMessage(messages.videoTypeButton),
|
||||
disabled: false,
|
||||
disabled: !isBlockTypeEnabled('video'),
|
||||
icon: VideoCamera,
|
||||
blockType: 'video',
|
||||
},
|
||||
{
|
||||
name: intl.formatMessage(messages.otherTypeButton),
|
||||
disabled: true,
|
||||
disabled: !isBlockTypeEnabled('other'),
|
||||
icon: AutoAwesome,
|
||||
blockType: 'other', // This block doesn't exist yet.
|
||||
},
|
||||
@@ -130,39 +145,49 @@ const AddContentContainer = () => {
|
||||
contentTypes.push(pasteButton);
|
||||
}
|
||||
|
||||
const onCreateContent = (blockType: string) => {
|
||||
if (libraryId) {
|
||||
if (blockType === 'paste') {
|
||||
pasteClipboardMutation.mutateAsync({
|
||||
libraryId,
|
||||
blockId: `${uuid4()}`,
|
||||
}).then(() => {
|
||||
showToast(intl.formatMessage(messages.successPasteClipboardMessage));
|
||||
}).catch(() => {
|
||||
showToast(intl.formatMessage(messages.errorPasteClipboardMessage));
|
||||
});
|
||||
} else if (blockType === 'collection') {
|
||||
openCreateCollectionModal();
|
||||
const onPaste = () => {
|
||||
if (!isBlockTypeEnabled(sharedClipboardData.content?.blockType)) {
|
||||
showToast(intl.formatMessage(messages.unsupportedBlockPasteClipboardMessage));
|
||||
return;
|
||||
}
|
||||
pasteClipboardMutation.mutateAsync({
|
||||
libraryId,
|
||||
blockId: `${uuid4()}`,
|
||||
}).then(() => {
|
||||
showToast(intl.formatMessage(messages.successPasteClipboardMessage));
|
||||
}).catch((error) => {
|
||||
showToast(parsePasteErrorMsg(error));
|
||||
});
|
||||
};
|
||||
|
||||
const onCreateBlock = (blockType: string) => {
|
||||
createBlockMutation.mutateAsync({
|
||||
libraryId,
|
||||
blockType,
|
||||
definitionId: `${uuid4()}`,
|
||||
}).then((data) => {
|
||||
const hasEditor = canEditComponent(data.id);
|
||||
updateComponentsMutation.mutateAsync([data.id]).catch(() => {
|
||||
showToast(intl.formatMessage(messages.errorAssociateComponentMessage));
|
||||
});
|
||||
if (hasEditor) {
|
||||
openComponentEditor(data.id);
|
||||
} else {
|
||||
createBlockMutation.mutateAsync({
|
||||
libraryId,
|
||||
blockType,
|
||||
definitionId: `${uuid4()}`,
|
||||
}).then((data) => {
|
||||
const hasEditor = canEditComponent(data.id);
|
||||
updateComponentsMutation.mutateAsync([data.id]).catch(() => {
|
||||
showToast(intl.formatMessage(messages.errorAssociateComponentMessage));
|
||||
});
|
||||
if (hasEditor) {
|
||||
openComponentEditor(data.id);
|
||||
} else {
|
||||
// We can't start editing this right away so just show a toast message:
|
||||
showToast(intl.formatMessage(messages.successCreateMessage));
|
||||
}
|
||||
}).catch(() => {
|
||||
showToast(intl.formatMessage(messages.errorCreateMessage));
|
||||
});
|
||||
// We can't start editing this right away so just show a toast message:
|
||||
showToast(intl.formatMessage(messages.successCreateMessage));
|
||||
}
|
||||
}).catch(() => {
|
||||
showToast(intl.formatMessage(messages.errorCreateMessage));
|
||||
});
|
||||
};
|
||||
|
||||
const onCreateContent = (blockType: string) => {
|
||||
if (blockType === 'paste') {
|
||||
onPaste();
|
||||
} else if (blockType === 'collection') {
|
||||
openCreateCollectionModal();
|
||||
} else {
|
||||
onCreateBlock(blockType);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -76,6 +76,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Pasting content from clipboard...',
|
||||
description: 'Message when in process of pasting content in library',
|
||||
},
|
||||
unsupportedBlockPasteClipboardMessage: {
|
||||
id: 'course-authoring.library-authoring.paste-clipboard.unsupportedblock-error.text',
|
||||
defaultMessage: 'Libraries do not support this type of content yet.',
|
||||
description: 'Message when unsupported block is pasted in library',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -45,6 +45,7 @@ mergeConfig({
|
||||
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL || null,
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL || null,
|
||||
LIBRARY_MODE: process.env.LIBRARY_MODE || 'v1 only',
|
||||
LIBRARY_SUPPORTED_BLOCKS: (process.env.LIBRARY_SUPPORTED_BLOCKS || 'problem,video,html').split(','),
|
||||
}, 'CourseAuthoringConfig');
|
||||
|
||||
class ResizeObserver {
|
||||
|
||||
Reference in New Issue
Block a user