Skip to content

Commit

Permalink
feat: [FC-0070] rendering library content in unit page
Browse files Browse the repository at this point in the history
  • Loading branch information
ihor-romaniuk committed Nov 28, 2024
1 parent 55fe87a commit 2af7492
Show file tree
Hide file tree
Showing 33 changed files with 498 additions and 184 deletions.
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const COURSE_BLOCK_NAMES = ({
chapter: { id: 'chapter', name: 'Section' },
sequential: { id: 'sequential', name: 'Subsection' },
vertical: { id: 'vertical', name: 'Unit' },
libraryContent: { id: 'library_content', name: 'Library content' },
component: { id: 'component', name: 'Component' },
});

Expand Down
73 changes: 41 additions & 32 deletions src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import Breadcrumbs from './breadcrumbs/Breadcrumbs';
import HeaderNavigations from './header-navigations/HeaderNavigations';
import Sequence from './course-sequence';
import Sidebar from './sidebar';
import { useCourseUnit } from './hooks';
import { useCourseUnit, useLayoutGrid } from './hooks';
import messages from './messages';
import PublishControls from './sidebar/PublishControls';
import LocationInfo from './sidebar/LocationInfo';
Expand All @@ -45,10 +45,13 @@ const CourseUnit = ({ courseId }) => {
isLoading,
sequenceId,
unitTitle,
unitCategory,
errorMessage,
sequenceStatus,
savingStatus,
isTitleEditFormOpen,
isUnitVerticalType,
isUnitLibraryType,
staticFileNotices,
currentlyVisibleToStudents,
unitXBlockActions,
Expand All @@ -70,6 +73,7 @@ const CourseUnit = ({ courseId }) => {
handleCloseXBlockMovedAlert,
handleNavigateToTargetUnit,
} = useCourseUnit({ courseId, blockId });
const layoutGrid = useLayoutGrid(unitCategory, isUnitLibraryType);

useEffect(() => {
document.title = getPageHeadTitle('', unitTitle);
Expand Down Expand Up @@ -142,28 +146,28 @@ const CourseUnit = ({ courseId }) => {
/>
)}
breadcrumbs={(
<Breadcrumbs />
<Breadcrumbs
courseId={courseId}
parentUnitId={sequenceId}
/>
)}
headerActions={(
<HeaderNavigations
unitCategory={unitCategory}
headerNavigationsActions={headerNavigationsActions}
/>
)}
/>
<Sequence
courseId={courseId}
sequenceId={sequenceId}
unitId={blockId}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
showPasteUnit={showPasteUnit}
/>
<Layout
lg={[{ span: 8 }, { span: 4 }]}
md={[{ span: 8 }, { span: 4 }]}
sm={[{ span: 8 }, { span: 3 }]}
xs={[{ span: 9 }, { span: 3 }]}
xl={[{ span: 9 }, { span: 3 }]}
>
{isUnitVerticalType && (
<Sequence
courseId={courseId}
sequenceId={sequenceId}
unitId={blockId}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
showPasteUnit={showPasteUnit}
/>
)}
<Layout {...layoutGrid}>
<Layout.Element>
{currentlyVisibleToStudents && (
<AlertMessage
Expand All @@ -184,11 +188,13 @@ const CourseUnit = ({ courseId }) => {
unitXBlockActions={unitXBlockActions}
courseVerticalChildren={courseVerticalChildren.children}
/>
<AddComponent
blockId={blockId}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
/>
{showPasteXBlock && canPasteComponent && (
{isUnitVerticalType && (
<AddComponent
blockId={blockId}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
/>
)}
{showPasteXBlock && canPasteComponent && isUnitVerticalType && (
<PasteComponent
clipboardData={sharedClipboardData}
onClick={handleCreateNewCourseXBlock}
Expand All @@ -205,18 +211,21 @@ const CourseUnit = ({ courseId }) => {
</Layout.Element>
<Layout.Element>
<Stack gap={3}>
<Sidebar data-testid="course-unit-sidebar">
<PublishControls blockId={blockId} />
</Sidebar>
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true'
&& (
<Sidebar className="tags-sidebar">
<TagsSidebarControls />
</Sidebar>
{isUnitVerticalType && (
<>
<Sidebar data-testid="course-unit-sidebar">
<PublishControls blockId={blockId} />
</Sidebar>
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
<Sidebar className="tags-sidebar">
<TagsSidebarControls />
</Sidebar>
)}
<Sidebar data-testid="course-unit-location-sidebar">
<LocationInfo />
</Sidebar>
</>
)}
<Sidebar data-testid="course-unit-location-sidebar">
<LocationInfo />
</Sidebar>
</Stack>
</Layout.Element>
</Layout>
Expand Down
4 changes: 4 additions & 0 deletions src/course-unit/CourseUnit.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
@import "./move-modal";
@import "./preview-changes";

.course-unit {
min-width: 900px;
}

.course-unit__alert {
margin-bottom: 1.75rem;
}
98 changes: 94 additions & 4 deletions src/course-unit/CourseUnit.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import sidebarMessages from './sidebar/messages';
import { extractCourseUnitId } from './sidebar/utils';
import CourseUnit from './CourseUnit';

import { getClipboardUrl } from '../generic/data/api';
import configureModalMessages from '../generic/configure-modal/messages';
import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api';
import addComponentMessages from './add-component/messages';
Expand Down Expand Up @@ -114,6 +115,22 @@ const clipboardBroadcastChannelMock = {

global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);

/**
* Simulates receiving a post message event for testing purposes.
* This can be used to mimic events like deletion or other actions
* sent from Backbone or other sources via postMessage.
*
* @param {string} type - The type of the message event (e.g., 'deleteXBlock').
* @param {Object} payload - The payload data for the message event.
*/
function simulatePostMessageEvent(type, payload) {
const messageEvent = new MessageEvent('message', {
data: { type, payload },
});

window.dispatchEvent(messageEvent);
}

const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en">
Expand All @@ -138,6 +155,9 @@ describe('<CourseUnit />', () => {
global.localStorage.clear();
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(getClipboardUrl())
.reply(200, clipboardUnit);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, courseUnitIndexMock);
Expand Down Expand Up @@ -226,6 +246,19 @@ describe('<CourseUnit />', () => {
display_name: newDisplayName,
},
});
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
display_name: newDisplayName,
},
xblock: {
...courseSectionVerticalMock.xblock,
display_name: newDisplayName,
},
});

await waitFor(() => {
const unitHeaderTitle = getByTestId('unit-header-title');
Expand Down Expand Up @@ -914,9 +947,7 @@ describe('<CourseUnit />', () => {
.reply(200, clipboardMockResponse);
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...updatedCourseSectionVerticalData,
});
.reply(200, updatedCourseSectionVerticalData);

global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
Expand Down Expand Up @@ -1190,7 +1221,7 @@ describe('<CourseUnit />', () => {

axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, {});
.reply(200, courseUnitIndexMock);

await act(async () => {
await waitFor(() => {
Expand Down Expand Up @@ -1324,4 +1355,63 @@ describe('<CourseUnit />', () => {
);
});
});

describe('Library Content page', () => {
const newUnitId = '12345';
const sequenceId = courseSectionVerticalMock.subsection_location;

beforeEach(async () => {
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
xblock: {
...courseSectionVerticalMock.xblock,
category: 'library_content',
},
xblock_info: {
...courseSectionVerticalMock.xblock_info,
category: 'library_content',
},
});
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
});

it('navigates to library content page on receive window event', () => {
render(<RootWrapper />);

simulatePostMessageEvent(messageTypes.handleViewXBlockContent, {
destination: `http://localhost:18001/container/${newUnitId}`,
});
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/container/${newUnitId}/${sequenceId}`);
});

it('should render library content page correctly', async () => {
const {
getByText,
getByRole,
queryByRole,
getByTestId,
} = render(<RootWrapper />);

const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;

await waitFor(() => {
const unitHeaderTitle = getByTestId('unit-header-title');
expect(getByText(unitDisplayName)).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();

expect(queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument();
expect(queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument();
expect(queryByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).not.toBeInTheDocument();

expect(queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument();
expect(queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument();
});
});
});
});
2 changes: 1 addition & 1 deletion src/course-unit/add-component/AddComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
const { componentTemplates } = useSelector(getCourseSectionVertical);
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
const [selectedComponents, setSelectedComponents] = useState([]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import { Icon } from '@openedx/paragon';
import { EditNote as EditNoteIcon } from '@openedx/paragon/icons';

import { COMPONENT_TYPES, COMPONENT_TYPE_ICON_MAP } from '../../../generic/block-type-utils/constants';
import { COMPONENT_TYPE_ICON_MAP } from '../../../generic/block-type-utils/constants';

const AddComponentIcon = ({ type }) => {
const icon = COMPONENT_TYPE_ICON_MAP[type] || EditNoteIcon;
Expand All @@ -11,7 +11,7 @@ const AddComponentIcon = ({ type }) => {
};

AddComponentIcon.propTypes = {
type: PropTypes.oneOf(Object.values(COMPONENT_TYPES)).isRequired,
type: PropTypes.string.isRequired,
};

export default AddComponentIcon;
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const ComponentModalView = ({
<OverlayTrigger
placement="right"
overlay={(
<Tooltip>
<Tooltip id={`${componentTemplate.displayName}-support-tooltip`}>
{supportLabels[componentTemplate.supportLevel].tooltip}
</Tooltip>
)}
Expand Down
Loading

0 comments on commit 2af7492

Please sign in to comment.