Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/course-unit/add-component/AddComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,41 @@ describe('<AddComponent />', () => {
expect(screen.getByRole('button')).not.toBeDisabled();
});

it('passes boilerplate name when clicking a generic container block button', async () => {
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
component_templates: [
{
type: 'pb-message',
display_name: 'Message (complete)',
templates: [{
display_name: 'Message (complete)',
category: 'pb-message',
boilerplate_name: 'pb-message-complete',
}],
support_legend: { show_legend: false },
},
],
});
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);

const user = userEvent.setup();
renderComponent();

await user.click(screen.getByRole('button', {
name: new RegExp(`${messages.buttonText.defaultMessage} Message \\(complete\\)`, 'i'),
}));

expect(handleCreateNewCourseXBlockMock).toHaveBeenCalledWith({
type: 'pb-message',
category: 'pb-message',
boilerplate: 'pb-message-complete',
parentLocator: blockId,
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new test covers the happy path (boilerplate present → forwarded). Consider adding a sibling test for the inverse: a generic container block whose template has no boilerplate_name.

describe('component support label', () => {
it('component support label is hidden if component support legend is disabled', async () => {
const supportLevels = ['fs', 'ps'];
Expand Down
20 changes: 14 additions & 6 deletions src/course-unit/add-component/AddComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useEventListener } from '@src/generic/hooks';
import VideoSelectorPage from '@src/editors/VideoSelectorPage';
import EditorPage from '@src/editors/EditorPage';
import { SelectedComponent } from '@src/library-authoring';
import { type CreateCourseXBlockParams } from '@src/course-unit/hooks';
import { fetchCourseSectionVerticalData } from '../data/thunk';
import { messageTypes } from '../constants';
import messages from './messages';
Expand All @@ -31,7 +32,7 @@ type ComponentTemplateData = {
type: string;
beta?: boolean;
templates: Array<{
boilerplateName?: string;
boilerplateName?: string | null;
category?: string;
displayName: string;
supportLevel?: string | boolean;
Expand All @@ -57,7 +58,7 @@ export interface AddComponentProps {
isGenericContainerType?: boolean;
parentLocator: string;
handleCreateNewCourseXBlock: (
args: object,
args: CreateCourseXBlockParams,
callback?: (args: { courseKey: string; locator: string; }) => void,
) => void;
isProblemBankType?: boolean;
Expand Down Expand Up @@ -148,7 +149,7 @@ const AddComponent = ({
closeAddLibraryContentModal();
}, [usageId]);

const handleCreateNewXBlock = (type: string, moduleName?: string) => {
const handleCreateNewXBlock = (type: string, moduleName?: string | null) => {
switch (type) {
case COMPONENT_TYPES.discussion:
case COMPONENT_TYPES.dragAndDrop:
Expand Down Expand Up @@ -209,7 +210,7 @@ const AddComponent = ({
},
);
} else {
handleCreateNewCourseXBlock({ type: moduleName, category: moduleName, parentLocator: blockId });
handleCreateNewCourseXBlock({ type: moduleName!, category: moduleName!, parentLocator: blockId });
}
break;
case COMPONENT_TYPES.openassessment:
Expand All @@ -231,7 +232,14 @@ const AddComponent = ({
// Generic handler for container-specific block types declared via
// StudioContainerWithNestedXBlocksMixin.allowed_nested_blocks that are
// not one of the MFE's built-in component types (html, video, problem…).
handleCreateNewCourseXBlock({ type, category: type, parentLocator: blockId });
// Pass boilerplate so that blocks with multiple templates (e.g. pb-message)
// get initialised with the correct template content instead of the default.
handleCreateNewCourseXBlock({
type,
category: type,
boilerplate: moduleName,
parentLocator: blockId,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • boilerplate: moduleName is always added to the object, even when moduleName is undefined. In JS, 'boilerplate' in obj returns false when the key is absent and true when it is present with value undefined
  • This follows the same pattern as the existing openassessment and html cases in this file, so the PR code is in parity.
  • But consider applying it to the PR code ...(moduleName ? { boilerplate: moduleName } : {}).

});
}
};

Expand Down Expand Up @@ -283,7 +291,7 @@ const AddComponent = ({
return (
<li key={type}>
<AddComponentButton
onClick={() => handleCreateNewXBlock(type)}
onClick={() => handleCreateNewXBlock(type, firstTemplate?.boilerplateName)}
displayName={displayName}
type={type}
beta={beta}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import messages from '../messages';
import ModalContainer from './ModalContainer';

interface ComponentTemplate {
boilerplateName?: string;
boilerplateName?: string | null;
category?: string;
displayName: string;
supportLevel?: string | boolean;
Expand Down
21 changes: 20 additions & 1 deletion src/course-unit/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,12 +285,31 @@ export const useCourseUnit = ({
};
};

export interface CreateCourseXBlockParams {
/** The usage key string of the parent that will contain this new block */
parentLocator: string;
/** The XBlock type to create. Note: a "type" field is ignored - don't add that here. */
category?: string;
/**
* The XBlock type to create. Ignored on the backend, but our frontend code accepts it instead of `category` and
* sets `category` to this value.
* @deprecated
*/
type?: string;
/** Template to use for the XBlock's initial content */
boilerplate?: string | null;
/** If we're linking in a block from a library, this is the upstream usage key in that library */
libraryContentKey?: string;
/** Specify this to paste from clipboard rather than creating a blank component. */
stagedContent?: 'clipboard';
}

export const useHandleCreateNewCourseXBlock = ({ blockId }: { blockId: string; }) => {
const dispatch = useDispatch();
const { sendMessageToIframe } = useIframe();

// oxlint-disable typescript-eslint(await-thenable)
return async (body: object, callback?: (args: { courseKey: string; locator: string; }) => void) => (
return async (body: CreateCourseXBlockParams, callback?: (args: { courseKey: string; locator: string; }) => void) => (
// eslint-disable-next-line @typescript-eslint/return-await
await dispatch(createNewCourseXBlock(body, callback, blockId, sendMessageToIframe))
);
Expand Down