feat: New Unit info sidebar [FC-0114] (#2822)
- Implements the basics for the Unit Sidebar:
- Splits the sidebar in legacy sidebar and in the new sidebar
- Implements the Unit Info Sidebar:
- Implements a new design for the visibility and publish status card.
- Implements the new Visibility field.
- Implements the settings tab for the sidebar. Implements all the new form to edit the
settings in the sidebar.
This commit is contained in:
@@ -39,9 +39,9 @@ ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
|
||||
ENABLE_CERTIFICATE_PAGE=true
|
||||
ENABLE_COURSE_IMPORT_IN_LIBRARY=true
|
||||
ENABLE_COURSE_OUTLINE_NEW_DESIGN=true
|
||||
ENABLE_UNIT_PAGE_NEW_DESIGN=true
|
||||
ENABLE_NEW_VIDEO_UPLOAD_PAGE=true
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES=true
|
||||
ENABLE_UNIT_PAGE_NEW_DESIGN=true
|
||||
BBB_LEARN_MORE_URL=''
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION=6
|
||||
|
||||
@@ -35,8 +35,8 @@ ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
|
||||
ENABLE_CERTIFICATE_PAGE=true
|
||||
ENABLE_COURSE_IMPORT_IN_LIBRARY=true
|
||||
ENABLE_COURSE_OUTLINE_NEW_DESIGN=false
|
||||
ENABLE_UNIT_PAGE_NEW_DESIGN=false
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES=true
|
||||
ENABLE_UNIT_PAGE_NEW_DESIGN=true
|
||||
BBB_LEARN_MORE_URL=''
|
||||
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
||||
ENABLE_CHECKLIST_QUALITY=true
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
@import "./breadcrumbs/Breadcrumbs";
|
||||
@import "./course-sequence/CourseSequence";
|
||||
@import "./add-component/AddComponent";
|
||||
@import "./sidebar/Sidebar";
|
||||
@import "./legacy-sidebar/LegacySidebar";
|
||||
@import "./unit-sidebar/unit-info/PublishControls";
|
||||
@import "./header-title/HeaderTitle";
|
||||
@import "./move-modal";
|
||||
@import "./preview-changes";
|
||||
|
||||
@@ -46,7 +46,7 @@ import { executeThunk } from '../utils';
|
||||
import pasteNotificationsMessages from './clipboard/paste-notification/messages';
|
||||
import headerTitleMessages from './header-title/messages';
|
||||
import courseSequenceMessages from './course-sequence/messages';
|
||||
import { extractCourseUnitId } from './sidebar/utils';
|
||||
import { extractCourseUnitId } from './legacy-sidebar/utils';
|
||||
import CourseUnit from './CourseUnit';
|
||||
|
||||
import tagsDrawerMessages from '../content-tags-drawer/messages';
|
||||
@@ -57,7 +57,8 @@ import { messageTypes, PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants
|
||||
import moveModalMessages from './move-modal/messages';
|
||||
import xblockContainerIframeMessages from './xblock-container-iframe/messages';
|
||||
import headerNavigationsMessages from './header-navigations/messages';
|
||||
import sidebarMessages from './sidebar/messages';
|
||||
import legacySidebarMessages from './legacy-sidebar/messages';
|
||||
import unitInfoMessages from './unit-sidebar/unit-info/messages';
|
||||
import messages from './messages';
|
||||
|
||||
let axiosMock;
|
||||
@@ -142,31 +143,27 @@ describe('<CourseUnit />', () => {
|
||||
const currentSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
|
||||
const currentSubSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
|
||||
|
||||
await waitFor(() => {
|
||||
const unitHeaderTitle = screen.getByTestId('unit-header-title');
|
||||
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
|
||||
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
|
||||
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: currentSectionName })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
|
||||
});
|
||||
const unitHeaderTitle = await screen.findByTestId('unit-header-title');
|
||||
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
|
||||
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
|
||||
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: currentSectionName })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the course unit iframe with correct attributes', async () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`);
|
||||
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
|
||||
expect(iframe).toHaveAttribute('style', 'height: 0px;');
|
||||
expect(iframe).toHaveAttribute('scrolling', 'no');
|
||||
expect(iframe).toHaveAttribute('referrerpolicy', 'origin');
|
||||
expect(iframe).toHaveAttribute('loading', 'lazy');
|
||||
expect(iframe).toHaveAttribute('frameborder', '0');
|
||||
});
|
||||
const iframe = await screen.findByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`);
|
||||
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
|
||||
expect(iframe).toHaveAttribute('style', 'height: 0px;');
|
||||
expect(iframe).toHaveAttribute('scrolling', 'no');
|
||||
expect(iframe).toHaveAttribute('referrerpolicy', 'origin');
|
||||
expect(iframe).toHaveAttribute('loading', 'lazy');
|
||||
expect(iframe).toHaveAttribute('frameborder', '0');
|
||||
});
|
||||
|
||||
it('adjusts iframe height dynamically based on courseXBlockDropdownHeight postMessage event', async () => {
|
||||
@@ -184,29 +181,25 @@ describe('<CourseUnit />', () => {
|
||||
it('displays an error alert when a studioAjaxError message is received', async () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.studioAjaxError, {
|
||||
error: 'Some error text...',
|
||||
});
|
||||
const xblocksIframe = await screen.findByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.studioAjaxError, {
|
||||
error: 'Some error text...',
|
||||
});
|
||||
expect(screen.getByTestId('saving-error-alert')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('saving-error-alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders XBlock iframe and opens legacy edit modal on editXBlock message', async () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.editXBlock, { id: blockId });
|
||||
const xblocksIframe = await screen.findByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.editXBlock, { id: blockId });
|
||||
|
||||
const legacyXBlockEditModalIframe = screen.getByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).toBeInTheDocument();
|
||||
});
|
||||
const legacyXBlockEditModalIframe = await screen.findByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the xBlocks iframe and opens the tags drawer on postMessage event', async () => {
|
||||
@@ -222,31 +215,27 @@ describe('<CourseUnit />', () => {
|
||||
it('closes the legacy edit modal when closeXBlockEditorModal message is received', async () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.closeXBlockEditorModal, { id: blockId });
|
||||
const xblocksIframe = await screen.findByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.closeXBlockEditorModal, { id: blockId });
|
||||
|
||||
const legacyXBlockEditModalIframe = screen.queryByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
|
||||
});
|
||||
const legacyXBlockEditModalIframe = screen.queryByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes legacy edit modal and updates course unit sidebar after saveEditedXBlockData message', async () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.saveEditedXBlockData);
|
||||
const xblocksIframe = await screen.findByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.saveEditedXBlockData);
|
||||
|
||||
const legacyXBlockEditModalIframe = screen.queryByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
|
||||
});
|
||||
const legacyXBlockEditModalIframe = screen.queryByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
@@ -259,33 +248,26 @@ describe('<CourseUnit />', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.releaseStatusTitle.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.sidebarBodyNote.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).queryByRole('button', {
|
||||
name: sidebarMessages.actionButtonPublishTitle.defaultMessage,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
const courseUnitSidebar = await screen.findByTestId('course-unit-sidebar');
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(legacySidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(legacySidebarMessages.releaseStatusTitle.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).queryByRole('button', {
|
||||
name: legacySidebarMessages.actionButtonPublishTitle.defaultMessage,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates course unit sidebar after receiving refreshPositions message', async () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.refreshPositions);
|
||||
});
|
||||
const xblocksIframe = await screen.findByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.refreshPositions);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
@@ -298,23 +280,18 @@ describe('<CourseUnit />', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.releaseStatusTitle.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.sidebarBodyNote.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).queryByRole('button', {
|
||||
name: sidebarMessages.actionButtonPublishTitle.defaultMessage,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
const courseUnitSidebar = await screen.findByTestId('course-unit-sidebar');
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(legacySidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(legacySidebarMessages.releaseStatusTitle.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).queryByRole('button', {
|
||||
name: legacySidebarMessages.actionButtonPublishTitle.defaultMessage,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => {
|
||||
@@ -373,13 +350,13 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// check if the sidebar status is Published and Live
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
unitInfoMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: legacySidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -421,22 +398,24 @@ describe('<CourseUnit />', () => {
|
||||
.replace('{xblockCount}', updatedCourseVerticalChildren.length),
|
||||
);
|
||||
// after removing the xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
legacySidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage,
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(unitInfoMessages.visibilityVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(unitInfoMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
legacySidebarMessages.actionButtonDiscardChangesTitle.defaultMessage,
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
unitInfoMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
legacySidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
@@ -508,7 +487,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }),
|
||||
await screen.findByRole('button', { name: legacySidebarMessages.actionButtonPublishTitle.defaultMessage }),
|
||||
);
|
||||
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
@@ -542,13 +521,13 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// check if the sidebar status is Published and Live
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
unitInfoMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: legacySidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -565,22 +544,22 @@ describe('<CourseUnit />', () => {
|
||||
);
|
||||
|
||||
// after duplicate the xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
legacySidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage,
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(unitInfoMessages.visibilityVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(unitInfoMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
unitInfoMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
legacySidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
@@ -699,7 +678,7 @@ describe('<CourseUnit />', () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(async () => {
|
||||
await user.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
await user.click(screen.getByRole('button', { name: legacySidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
});
|
||||
|
||||
axiosMock
|
||||
@@ -737,22 +716,22 @@ describe('<CourseUnit />', () => {
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
// after creating problem xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
legacySidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage,
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(unitInfoMessages.visibilityVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(unitInfoMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
unitInfoMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
legacySidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
@@ -884,7 +863,7 @@ describe('<CourseUnit />', () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(async () => {
|
||||
await user.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
await user.click(screen.getByRole('button', { name: legacySidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
});
|
||||
|
||||
axiosMock
|
||||
@@ -908,15 +887,15 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// check if the sidebar status is Published and Live
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
unitInfoMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: legacySidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
|
||||
const videoButton = screen.getByRole('button', {
|
||||
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
|
||||
@@ -932,22 +911,22 @@ describe('<CourseUnit />', () => {
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
legacySidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage,
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(unitInfoMessages.visibilityVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(unitInfoMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
unitInfoMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
legacySidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /add video to your course/i, hidden: true })).toBeInTheDocument();
|
||||
@@ -962,7 +941,7 @@ describe('<CourseUnit />', () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(async () => {
|
||||
await user.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
await user.click(screen.getByRole('button', { name: legacySidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
});
|
||||
|
||||
axiosMock
|
||||
@@ -986,13 +965,13 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await waitFor(async () => {
|
||||
// check if the sidebar status is Published and Live
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
unitInfoMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: legacySidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
|
||||
const videoButton = screen.getByRole('button', {
|
||||
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
|
||||
@@ -1015,22 +994,22 @@ describe('<CourseUnit />', () => {
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
legacySidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage,
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(unitInfoMessages.visibilityVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(unitInfoMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
unitInfoMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
legacySidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
@@ -1039,22 +1018,24 @@ describe('<CourseUnit />', () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
legacySidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage,
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(unitInfoMessages.visibilityVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(unitInfoMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
legacySidebarMessages.actionButtonDiscardChangesTitle.defaultMessage,
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
unitInfoMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
legacySidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
@@ -1065,10 +1046,10 @@ describe('<CourseUnit />', () => {
|
||||
const courseUnitLocationId = extractCourseUnitId(courseSectionVerticalMock.xblock_info.id);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(legacySidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(courseUnitLocationId)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.unitLocationDescription.defaultMessage
|
||||
expect(screen.getByText(legacySidebarMessages.unitLocationDescription.defaultMessage
|
||||
.replace('{id}', courseUnitLocationId))).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1119,20 +1100,18 @@ describe('<CourseUnit />', () => {
|
||||
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
|
||||
draftUnpublishedChangesHeading = within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
|
||||
.getByText(legacySidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
|
||||
expect(draftUnpublishedChangesHeading).toBeInTheDocument();
|
||||
|
||||
visibilityCheckbox = within(courseUnitSidebar)
|
||||
.getByLabelText(sidebarMessages.visibilityCheckboxTitle.defaultMessage);
|
||||
.getByLabelText(unitInfoMessages.visibilityCheckboxTitle.defaultMessage);
|
||||
expect(visibilityCheckbox).not.toBeChecked();
|
||||
|
||||
await user.click(visibilityCheckbox);
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(blockId), {
|
||||
publish: PUBLISH_TYPES.republish,
|
||||
metadata: { visible_to_staff_only: true, group_access: null },
|
||||
metadata: { visible_to_staff_only: true },
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
@@ -1148,31 +1127,33 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, true), store.dispatch);
|
||||
|
||||
expect(visibilityCheckbox).toBeChecked();
|
||||
await waitFor(async () => {
|
||||
expect(visibilityCheckbox).toBeChecked();
|
||||
});
|
||||
expect(within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.sidebarTitleVisibleToStaffOnly.defaultMessage)).toBeInTheDocument();
|
||||
.getByText(legacySidebarMessages.sidebarTitleVisibleToStaffOnly.defaultMessage)).toBeInTheDocument();
|
||||
expect(within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.visibilityStaffOnlyTitle.defaultMessage)).toBeInTheDocument();
|
||||
.getByText(unitInfoMessages.visibilityStaffOnlyTitle.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
await user.click(visibilityCheckbox);
|
||||
|
||||
const modalNotification = screen.getByRole('dialog');
|
||||
const makeVisibilityBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityActionButtonText.defaultMessage });
|
||||
const cancelBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityCancelButtonText.defaultMessage });
|
||||
const headingElement = within(modalNotification).getByRole('heading', { name: sidebarMessages.modalMakeVisibilityTitle.defaultMessage, class: 'pgn__modal-title' });
|
||||
const makeVisibilityBtn = within(modalNotification).getByRole('button', { name: unitInfoMessages.modalMakeVisibilityActionButtonText.defaultMessage });
|
||||
const cancelBtn = within(modalNotification).getByRole('button', { name: unitInfoMessages.modalMakeVisibilityCancelButtonText.defaultMessage });
|
||||
const headingElement = within(modalNotification).getByRole('heading', { name: unitInfoMessages.modalMakeVisibilityTitle.defaultMessage, class: 'pgn__modal-title' });
|
||||
|
||||
expect(makeVisibilityBtn).toBeInTheDocument();
|
||||
expect(cancelBtn).toBeInTheDocument();
|
||||
expect(headingElement).toBeInTheDocument();
|
||||
expect(within(modalNotification)
|
||||
.getByText(sidebarMessages.modalMakeVisibilityDescription.defaultMessage)).toBeInTheDocument();
|
||||
.getByText(unitInfoMessages.modalMakeVisibilityDescription.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
await user.click(makeVisibilityBtn);
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(blockId), {
|
||||
publish: PUBLISH_TYPES.republish,
|
||||
metadata: { visible_to_staff_only: null, group_access: null },
|
||||
metadata: { visible_to_staff_only: null },
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
@@ -1181,8 +1162,6 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, null), store.dispatch);
|
||||
|
||||
expect(within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(visibilityCheckbox).not.toBeChecked();
|
||||
expect(draftUnpublishedChangesHeading).toBeInTheDocument();
|
||||
});
|
||||
@@ -1195,7 +1174,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await waitFor(async () => {
|
||||
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
publishBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage });
|
||||
publishBtn = within(courseUnitSidebar).queryByRole('button', { name: legacySidebarMessages.actionButtonPublishTitle.defaultMessage });
|
||||
expect(publishBtn).toBeInTheDocument();
|
||||
|
||||
await user.click(publishBtn);
|
||||
@@ -1221,9 +1200,9 @@ describe('<CourseUnit />', () => {
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
expect(within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
.getByText(legacySidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(within(courseUnitSidebar).getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
unitInfoMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
@@ -1240,9 +1219,9 @@ describe('<CourseUnit />', () => {
|
||||
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
|
||||
const draftUnpublishedChangesHeading = within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
|
||||
.getByText(legacySidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
|
||||
expect(draftUnpublishedChangesHeading).toBeInTheDocument();
|
||||
discardChangesBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage });
|
||||
discardChangesBtn = await within(courseUnitSidebar).findByRole('button', { name: legacySidebarMessages.actionButtonDiscardChangesTitle.defaultMessage });
|
||||
expect(discardChangesBtn).toBeInTheDocument();
|
||||
|
||||
await user.click(discardChangesBtn);
|
||||
@@ -1250,12 +1229,12 @@ describe('<CourseUnit />', () => {
|
||||
const modalNotification = screen.getByRole('dialog');
|
||||
expect(modalNotification).toBeInTheDocument();
|
||||
expect(within(modalNotification)
|
||||
.getByText(sidebarMessages.modalDiscardUnitChangesDescription.defaultMessage)).toBeInTheDocument();
|
||||
.getByText(unitInfoMessages.modalDiscardUnitChangesDescription.defaultMessage)).toBeInTheDocument();
|
||||
expect(within(modalNotification)
|
||||
.getByText(sidebarMessages.modalDiscardUnitChangesCancelButtonText.defaultMessage)).toBeInTheDocument();
|
||||
const headingElement = within(modalNotification).getByRole('heading', { name: sidebarMessages.modalDiscardUnitChangesTitle.defaultMessage, class: 'pgn__modal-title' });
|
||||
.getByText(unitInfoMessages.modalDiscardUnitChangesCancelButtonText.defaultMessage)).toBeInTheDocument();
|
||||
const headingElement = within(modalNotification).getByRole('heading', { name: unitInfoMessages.modalDiscardUnitChangesTitle.defaultMessage, class: 'pgn__modal-title' });
|
||||
expect(headingElement).toBeInTheDocument();
|
||||
const actionBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalDiscardUnitChangesActionButtonText.defaultMessage });
|
||||
const actionBtn = within(modalNotification).getByRole('button', { name: unitInfoMessages.modalDiscardUnitChangesActionButtonText.defaultMessage });
|
||||
expect(actionBtn).toBeInTheDocument();
|
||||
|
||||
await user.click(actionBtn);
|
||||
@@ -1284,7 +1263,7 @@ describe('<CourseUnit />', () => {
|
||||
), store.dispatch);
|
||||
|
||||
expect(within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.sidebarTitlePublishedNotYetReleased.defaultMessage)).toBeInTheDocument();
|
||||
.getByText(legacySidebarMessages.sidebarTitlePublishedNotYetReleased.defaultMessage)).toBeInTheDocument();
|
||||
expect(discardChangesBtn).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -1300,7 +1279,7 @@ describe('<CourseUnit />', () => {
|
||||
await waitFor(async () => {
|
||||
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
sidebarVisibilityCheckbox = within(courseUnitSidebar)
|
||||
.getByLabelText(sidebarMessages.visibilityCheckboxTitle.defaultMessage);
|
||||
.getByLabelText(unitInfoMessages.visibilityCheckboxTitle.defaultMessage);
|
||||
expect(sidebarVisibilityCheckbox).not.toBeChecked();
|
||||
|
||||
const headerConfigureBtn = screen.getByRole('button', { name: /settings/i });
|
||||
@@ -1355,9 +1334,9 @@ describe('<CourseUnit />', () => {
|
||||
await waitFor(() => {
|
||||
expect(sidebarVisibilityCheckbox).toBeChecked();
|
||||
expect(within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.sidebarTitleVisibleToStaffOnly.defaultMessage)).toBeInTheDocument();
|
||||
.getByText(legacySidebarMessages.sidebarTitleVisibleToStaffOnly.defaultMessage)).toBeInTheDocument();
|
||||
expect(within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.visibilityStaffOnlyTitle.defaultMessage)).toBeInTheDocument();
|
||||
.getByText(unitInfoMessages.visibilityStaffOnlyTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1396,7 +1375,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
await user.click(screen.getByRole('button', { name: legacySidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
await user.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
|
||||
let units = null;
|
||||
@@ -1453,7 +1432,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
await user.click(screen.getByRole('button', { name: legacySidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
@@ -1517,7 +1496,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
await user.click(screen.getByRole('button', { name: legacySidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
await user.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
@@ -1570,7 +1549,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
await user.click(screen.getByRole('button', { name: legacySidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
await user.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
@@ -1625,7 +1604,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
await user.click(screen.getByRole('button', { name: legacySidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
await user.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
@@ -2210,41 +2189,41 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
// Sidebar
|
||||
const sidebarContent = [
|
||||
{ query: screen.queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestAddComponentTitle.defaultMessage },
|
||||
{ query: screen.queryByText, name: sidebarMessages.sidebarSplitTestSelectComponentType.defaultMessage.replaceAll('{bold_tag}', '') },
|
||||
{ query: screen.queryByText, name: sidebarMessages.sidebarSplitTestComponentAdded.defaultMessage },
|
||||
{ query: screen.queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestEditComponentTitle.defaultMessage },
|
||||
{ query: screen.queryByRole, type: 'heading', name: legacySidebarMessages.sidebarSplitTestAddComponentTitle.defaultMessage },
|
||||
{ query: screen.queryByText, name: legacySidebarMessages.sidebarSplitTestSelectComponentType.defaultMessage.replaceAll('{bold_tag}', '') },
|
||||
{ query: screen.queryByText, name: legacySidebarMessages.sidebarSplitTestComponentAdded.defaultMessage },
|
||||
{ query: screen.queryByRole, type: 'heading', name: legacySidebarMessages.sidebarSplitTestEditComponentTitle.defaultMessage },
|
||||
{
|
||||
query: screen.queryByText,
|
||||
name: sidebarMessages.sidebarSplitTestEditComponentInstruction.defaultMessage
|
||||
name: legacySidebarMessages.sidebarSplitTestEditComponentInstruction.defaultMessage
|
||||
.replaceAll('{bold_tag}', ''),
|
||||
},
|
||||
{
|
||||
query: screen.queryByRole,
|
||||
type: 'heading',
|
||||
name: sidebarMessages.sidebarSplitTestReorganizeComponentTitle.defaultMessage,
|
||||
name: legacySidebarMessages.sidebarSplitTestReorganizeComponentTitle.defaultMessage,
|
||||
},
|
||||
{
|
||||
query: screen.queryByText,
|
||||
name: sidebarMessages.sidebarSplitTestReorganizeComponentInstruction.defaultMessage,
|
||||
name: legacySidebarMessages.sidebarSplitTestReorganizeComponentInstruction.defaultMessage,
|
||||
},
|
||||
{
|
||||
query: screen.queryByText,
|
||||
name: sidebarMessages.sidebarSplitTestReorganizeGroupsInstruction.defaultMessage,
|
||||
name: legacySidebarMessages.sidebarSplitTestReorganizeGroupsInstruction.defaultMessage,
|
||||
},
|
||||
{
|
||||
query: screen.queryByRole,
|
||||
type: 'heading',
|
||||
name: sidebarMessages.sidebarSplitTestExperimentComponentTitle.defaultMessage,
|
||||
name: legacySidebarMessages.sidebarSplitTestExperimentComponentTitle.defaultMessage,
|
||||
},
|
||||
{
|
||||
query: screen.queryByText,
|
||||
name: sidebarMessages.sidebarSplitTestExperimentComponentInstruction.defaultMessage,
|
||||
name: legacySidebarMessages.sidebarSplitTestExperimentComponentInstruction.defaultMessage,
|
||||
},
|
||||
{
|
||||
query: screen.queryByRole,
|
||||
type: 'link',
|
||||
name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage,
|
||||
name: legacySidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2253,7 +2232,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }),
|
||||
screen.queryByRole('link', { name: legacySidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }),
|
||||
).toHaveAttribute('href', helpLinkUrl);
|
||||
});
|
||||
});
|
||||
@@ -2338,7 +2317,7 @@ describe('<CourseUnit />', () => {
|
||||
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
const publishButton = within(courseUnitSidebar).getByRole(
|
||||
'button',
|
||||
{ name: sidebarMessages.actionButtonPublishTitle.defaultMessage },
|
||||
{ name: legacySidebarMessages.actionButtonPublishTitle.defaultMessage },
|
||||
);
|
||||
expect(publishButton).toBeInTheDocument();
|
||||
expect(publishButton).toBeEnabled();
|
||||
@@ -2347,6 +2326,34 @@ describe('<CourseUnit />', () => {
|
||||
expect(screen.queryByText(addComponentMessages.title.defaultMessage)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders new unit info/settings sidebar', async () => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_UNIT_PAGE_NEW_DESIGN: 'true',
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
|
||||
|
||||
expect(await screen.findByRole('tab', { name: /details/i })).toBeInTheDocument();
|
||||
const settingsTab = screen.getByRole('tab', { name: /settings/i });
|
||||
expect(settingsTab).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /unit content summary/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /taxonomy alignments/i })).toBeInTheDocument();
|
||||
|
||||
await user.click(settingsTab);
|
||||
|
||||
expect(screen.getByRole('heading', { name: /visibility/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /access restrictions/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /discussion/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the live state in the status bar', async () => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
@@ -2366,11 +2373,93 @@ describe('<CourseUnit />', () => {
|
||||
expect(await screen.findByText('Live')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should change the visibility of the unit in the settings sidebar', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
|
||||
|
||||
// Move to settings
|
||||
expect(await screen.findByRole('heading', { name: /draft \(unpublished changes\)/i })).toBeInTheDocument();
|
||||
const settingsTab = screen.getByRole('tab', { name: /settings/i });
|
||||
expect(settingsTab).toBeInTheDocument();
|
||||
await user.click(settingsTab);
|
||||
|
||||
// Change Visibility to Staff Only
|
||||
expect(screen.getByRole('heading', { name: /visibility/i })).toBeInTheDocument();
|
||||
const staffOnlyButton = screen.getByRole('button', { name: /staff only/i });
|
||||
expect(staffOnlyButton).toBeInTheDocument();
|
||||
await user.click(staffOnlyButton);
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(blockId), {
|
||||
publish: PUBLISH_TYPES.republish,
|
||||
metadata: { visible_to_staff_only: true },
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
|
||||
has_explicit_staff_lock: true,
|
||||
},
|
||||
});
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, true), store.dispatch);
|
||||
// Move to Details
|
||||
const detailsTab = screen.getByRole('tab', { name: /details/i });
|
||||
await user.click(detailsTab);
|
||||
expect(screen.getByRole('heading', { name: /visible to staff only/i })).toBeInTheDocument();
|
||||
|
||||
// Move to settings and change visibility to all
|
||||
const editVisibilityButton = screen.getByRole('button', { name: /edit visibility/i });
|
||||
await user.click(editVisibilityButton);
|
||||
const studentVisibleButton = screen.getByRole('button', { name: /student visible/i });
|
||||
await user.click(studentVisibleButton);
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(blockId), {
|
||||
publish: PUBLISH_TYPES.republish,
|
||||
metadata: {
|
||||
visible_to_staff_only: null,
|
||||
},
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
visibility_state: 'needs_attention',
|
||||
has_explicit_staff_lock: false,
|
||||
},
|
||||
});
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, false), store.dispatch);
|
||||
|
||||
// Move to Details
|
||||
await user.click(detailsTab);
|
||||
expect(
|
||||
screen.getByRole('heading', { name: /draft \(unpublished changes\)/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the staff only state in the status bar', async () => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_UNIT_PAGE_NEW_DESIGN: 'true',
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
@@ -2383,7 +2472,208 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
render(<RootWrapper />);
|
||||
expect(await screen.findByText('Staff Only')).toBeInTheDocument();
|
||||
// (1) Chip in the header.
|
||||
// (2) Status title in the unit sidebar.
|
||||
expect((await screen.findAllByText('Staff Only')).length).toBe(2);
|
||||
});
|
||||
|
||||
it('should disable discussions in the settings sidebar', async () => {
|
||||
const user = userEvent.setup();
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_UNIT_PAGE_NEW_DESIGN: 'true',
|
||||
});
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
|
||||
|
||||
// Move to settings
|
||||
expect(await screen.findByRole('heading', { name: /draft \(unpublished changes\)/i })).toBeInTheDocument();
|
||||
const settingsTab = screen.getByRole('tab', { name: /settings/i });
|
||||
expect(settingsTab).toBeInTheDocument();
|
||||
await user.click(settingsTab);
|
||||
|
||||
// Disable discussions
|
||||
const discussionButton = screen.getByRole('checkbox', { name: /enable discussion/i });
|
||||
expect(discussionButton).toBeChecked();
|
||||
await user.click(discussionButton);
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(blockId), {
|
||||
publish: PUBLISH_TYPES.republish,
|
||||
metadata: {
|
||||
visible_to_staff_only: null,
|
||||
discussion_enabled: false,
|
||||
},
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
discussion_enabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(
|
||||
blockId,
|
||||
PUBLISH_TYPES.republish,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
), store.dispatch);
|
||||
|
||||
expect(discussionButton).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('should one group in the visibility field in the unit sidebar', async () => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_UNIT_PAGE_NEW_DESIGN: 'true',
|
||||
});
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
});
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
user_partition_info: {
|
||||
selected_partition_index: 0,
|
||||
selected_groups_label: 'Group A',
|
||||
selectable_partitions: [{
|
||||
id: 10,
|
||||
name: 'Content Groups',
|
||||
scheme: 'cohort',
|
||||
groups: [
|
||||
{
|
||||
deleted: false,
|
||||
id: 1,
|
||||
name: 'Group A',
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
deleted: false,
|
||||
id: 2,
|
||||
name: 'Group B',
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
deleted: false,
|
||||
id: 3,
|
||||
name: 'Group C',
|
||||
selected: false,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId, courseId), store.dispatch);
|
||||
expect(await screen.findByRole('heading', { name: /draft \(unpublished changes\)/i })).toBeInTheDocument();
|
||||
expect(await screen.findByText(/this unit is restricted to group a and staff/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should multiple groups in the visibility field in the unit sidebar', async () => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_UNIT_PAGE_NEW_DESIGN: 'true',
|
||||
});
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
});
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
user_partition_info: {
|
||||
selected_partition_index: 0,
|
||||
selected_groups_label: 'Group A, Group B, Group C',
|
||||
selectable_partitions: [{
|
||||
id: 10,
|
||||
name: 'Content Groups',
|
||||
scheme: 'cohort',
|
||||
groups: [
|
||||
{
|
||||
deleted: false,
|
||||
id: 1,
|
||||
name: 'Group A',
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
deleted: false,
|
||||
id: 2,
|
||||
name: 'Group B',
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
deleted: false,
|
||||
id: 3,
|
||||
name: 'Group C',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId, courseId), store.dispatch);
|
||||
expect(await screen.findByRole('heading', { name: /draft \(unpublished changes\)/i })).toBeInTheDocument();
|
||||
expect(await screen.findByText(/access restrictions applied/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(
|
||||
/access to some content in this unit is restricted to specific groups of learners\./i,
|
||||
));
|
||||
});
|
||||
|
||||
it('should render never published state in the unit sidebar', async () => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_UNIT_PAGE_NEW_DESIGN: 'true',
|
||||
});
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
});
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
published: false,
|
||||
released_to_students: false,
|
||||
currently_visible_to_students: false,
|
||||
},
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId, courseId), store.dispatch);
|
||||
|
||||
// Move to settings
|
||||
expect(await screen.findByRole('heading', { name: /draft \(never published\)/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the scheduled state in the status bar', async () => {
|
||||
@@ -2457,9 +2747,33 @@ describe('<CourseUnit />', () => {
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
user_partition_info: {
|
||||
...courseSectionVerticalMock.xblock_info.user_partition_info,
|
||||
selected_partition_index: 1,
|
||||
selected_partition_index: 0,
|
||||
selected_groups_label: 'Visibility group 1',
|
||||
selectable_partitions: [{
|
||||
id: 10,
|
||||
name: 'Content Groups',
|
||||
scheme: 'cohort',
|
||||
groups: [
|
||||
{
|
||||
deleted: false,
|
||||
id: 1,
|
||||
name: 'Visibility group 1',
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
deleted: false,
|
||||
id: 2,
|
||||
name: 'Visibility group 2',
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
deleted: false,
|
||||
id: 3,
|
||||
name: 'Visibility group 3',
|
||||
selected: false,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -2480,9 +2794,33 @@ describe('<CourseUnit />', () => {
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
user_partition_info: {
|
||||
...courseSectionVerticalMock.xblock_info.user_partition_info,
|
||||
selected_partition_index: 1,
|
||||
selected_partition_index: 0,
|
||||
selected_groups_label: 'Visibility group 1, Visibility group 2, Visibility group 3',
|
||||
selectable_partitions: [{
|
||||
id: 10,
|
||||
name: 'Content Groups',
|
||||
scheme: 'cohort',
|
||||
groups: [
|
||||
{
|
||||
deleted: false,
|
||||
id: 1,
|
||||
name: 'Visibility group 1',
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
deleted: false,
|
||||
id: 2,
|
||||
name: 'Visibility group 2',
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
deleted: false,
|
||||
id: 3,
|
||||
name: 'Visibility group 3',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,10 @@ import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import type { MessageDescriptor } from 'react-intl';
|
||||
import {
|
||||
Alert, Container, Layout, Button, TransitionReplace,
|
||||
Alert,
|
||||
Container,
|
||||
Button,
|
||||
TransitionReplace,
|
||||
Stack,
|
||||
Badge,
|
||||
Icon,
|
||||
@@ -37,13 +40,14 @@ import AddComponent from './add-component/AddComponent';
|
||||
import HeaderTitle from './header-title/HeaderTitle';
|
||||
import Breadcrumbs from './breadcrumbs/Breadcrumbs';
|
||||
import Sequence from './course-sequence';
|
||||
import { useCourseUnit, useLayoutGrid, useScrollToLastPosition } from './hooks';
|
||||
import { useCourseUnit, useScrollToLastPosition } from './hooks';
|
||||
import messages from './messages';
|
||||
import { PasteNotificationAlert } from './clipboard';
|
||||
import XBlockContainerIframe from './xblock-container-iframe';
|
||||
import MoveModal from './move-modal';
|
||||
import IframePreviewLibraryXBlockChanges from './preview-changes';
|
||||
import CourseUnitHeaderActionsSlot from '../plugin-slots/CourseUnitHeaderActionsSlot';
|
||||
import { UnitSidebarProvider } from './unit-sidebar/UnitSidebarContext';
|
||||
import { UNIT_VISIBILITY_STATES } from './constants';
|
||||
import { isUnitPageNewDesignEnabled } from './utils';
|
||||
|
||||
@@ -159,9 +163,20 @@ const StatusBar = ({ courseUnit }: { courseUnit: any }) => {
|
||||
};
|
||||
|
||||
const CourseUnit = () => {
|
||||
const { blockId } = useParams();
|
||||
const intl = useIntl();
|
||||
const { blockId } = useParams();
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
|
||||
if (courseId === undefined) {
|
||||
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
|
||||
throw new Error('Error: route is missing courseId.');
|
||||
}
|
||||
|
||||
if (blockId === undefined) {
|
||||
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
|
||||
throw new Error('Error: route is missing blockId.');
|
||||
}
|
||||
|
||||
const {
|
||||
courseUnit,
|
||||
isLoading,
|
||||
@@ -173,7 +188,7 @@ const CourseUnit = () => {
|
||||
savingStatus,
|
||||
isTitleEditFormOpen,
|
||||
isUnitVerticalType,
|
||||
isUnitLibraryType,
|
||||
isUnitLegacyLibraryType,
|
||||
isSplitTestType,
|
||||
isProblemBankType,
|
||||
staticFileNotices,
|
||||
@@ -199,8 +214,6 @@ const CourseUnit = () => {
|
||||
addComponentTemplateData,
|
||||
} = useCourseUnit({ courseId, blockId });
|
||||
|
||||
const layoutGrid = useLayoutGrid(unitCategory, isUnitLibraryType);
|
||||
|
||||
const readOnly = !!courseUnit.readOnly;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -227,8 +240,8 @@ const CourseUnit = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container size="xl" className="course-unit px-4">
|
||||
<UnitSidebarProvider>
|
||||
<Container fluid className="course-unit px-4">
|
||||
<section className="course-unit-container mb-4 mt-5">
|
||||
<TransitionReplace>
|
||||
{movedXBlockParams.isSuccess ? (
|
||||
@@ -321,8 +334,8 @@ const CourseUnit = () => {
|
||||
showPasteUnit={showPasteUnit}
|
||||
/>
|
||||
)}
|
||||
<Layout {...layoutGrid}>
|
||||
<Layout.Element>
|
||||
<div className="d-flex align-items-baseline">
|
||||
<div className="flex-fill">
|
||||
{currentlyVisibleToStudents && (
|
||||
<AlertMessage
|
||||
className="course-unit__alert"
|
||||
@@ -373,21 +386,19 @@ const CourseUnit = () => {
|
||||
courseId={courseId}
|
||||
/>
|
||||
<IframePreviewLibraryXBlockChanges />
|
||||
</Layout.Element>
|
||||
<Layout.Element>
|
||||
{blockId && (
|
||||
<CourseAuthoringUnitSidebarSlot
|
||||
courseId={courseId}
|
||||
blockId={blockId}
|
||||
unitTitle={unitTitle}
|
||||
xBlocks={courseVerticalChildren.children}
|
||||
readOnly={readOnly}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
isSplitTestType={isSplitTestType}
|
||||
/>
|
||||
)}
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
</div>
|
||||
{!isUnitLegacyLibraryType && (
|
||||
<CourseAuthoringUnitSidebarSlot
|
||||
courseId={courseId}
|
||||
blockId={blockId}
|
||||
unitTitle={unitTitle}
|
||||
xBlocks={courseVerticalChildren.children}
|
||||
readOnly={readOnly}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
isSplitTestType={isSplitTestType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</Container>
|
||||
<div className="alert-toast">
|
||||
@@ -400,7 +411,7 @@ const CourseUnit = () => {
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</UnitSidebarProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import messages from './sidebar/messages';
|
||||
import messages from './legacy-sidebar/messages';
|
||||
import addComponentMessages from './add-component/messages';
|
||||
|
||||
export const getUnitReleaseStatus = (intl) => ({
|
||||
@@ -16,6 +16,9 @@ export const UNIT_VISIBILITY_STATES = {
|
||||
export const ICON_COLOR_VARIANTS = {
|
||||
BLACK: '#000',
|
||||
GREEN: '#0D7D4D',
|
||||
ORANGE: '#B4610E',
|
||||
PRIMARY: '#0A3055',
|
||||
INFO: '#006DAA',
|
||||
};
|
||||
|
||||
export const PUBLISH_TYPES = {
|
||||
|
||||
@@ -49,16 +49,16 @@ export async function handleCourseUnitVisibilityAndData(
|
||||
unitId: string,
|
||||
type: string, // The action type (e.g., PUBLISH_TYPES.discardChanges).
|
||||
isVisible: boolean, // The visibility status for students.
|
||||
groupAccess: boolean,
|
||||
isDiscussionEnabled: boolean,
|
||||
groupAccess: Record<string, any> | null,
|
||||
): Promise<object> {
|
||||
const body = {
|
||||
publish: groupAccess ? null : type,
|
||||
...(type === PUBLISH_TYPES.republish ? {
|
||||
metadata: {
|
||||
visible_to_staff_only: isVisible ? true : null,
|
||||
group_access: groupAccess || null,
|
||||
discussion_enabled: isDiscussionEnabled,
|
||||
...(groupAccess != null && { group_access: groupAccess }),
|
||||
},
|
||||
} : {}),
|
||||
};
|
||||
|
||||
@@ -117,8 +117,8 @@ export function editCourseUnitVisibilityAndData(
|
||||
itemId,
|
||||
type,
|
||||
isVisible,
|
||||
groupAccess,
|
||||
isDiscussionEnabled,
|
||||
groupAccess,
|
||||
).then(async (result) => {
|
||||
if (result) {
|
||||
if (callback) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
useCallback, useEffect, useMemo, useRef, useState,
|
||||
useCallback, useEffect, useRef, useState,
|
||||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
@@ -71,7 +71,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
const { displayName: unitTitle, category: unitCategory } = xblockInfo;
|
||||
const sequenceId = courseUnit.ancestorInfo?.ancestors[0].id;
|
||||
const isUnitVerticalType = unitCategory === COURSE_BLOCK_NAMES.vertical.id;
|
||||
const isUnitLibraryType = unitCategory === COURSE_BLOCK_NAMES.libraryContent.id;
|
||||
const isUnitLegacyLibraryType = unitCategory === COURSE_BLOCK_NAMES.libraryContent.id;
|
||||
const isSplitTestType = unitCategory === COURSE_BLOCK_NAMES.splitTest.id;
|
||||
const isProblemBankType = [
|
||||
COURSE_BLOCK_NAMES.legacyLibraryContent.id,
|
||||
@@ -256,7 +256,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
isLoading,
|
||||
isTitleEditFormOpen,
|
||||
isUnitVerticalType,
|
||||
isUnitLibraryType,
|
||||
isUnitLegacyLibraryType,
|
||||
isSplitTestType,
|
||||
isProblemBankType,
|
||||
sharedClipboardData,
|
||||
@@ -282,38 +282,6 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to determine the layout grid configuration based on unit category and type.
|
||||
*
|
||||
* @param {string} unitCategory - The category of the unit. This may influence future layout logic.
|
||||
* @param {boolean} isUnitLibraryType - A flag indicating whether the unit is of library content type.
|
||||
* @returns {Object} - An object representing the layout configuration for different screen sizes.
|
||||
* The configuration includes keys like 'lg', 'md', 'sm', 'xs', and 'xl',
|
||||
* each specifying an array of layout spans.
|
||||
*/
|
||||
export const useLayoutGrid = (unitCategory, isUnitLibraryType) => (
|
||||
useMemo(() => {
|
||||
const layouts = {
|
||||
fullWidth: {
|
||||
lg: [{ span: 12 }, { span: 0 }],
|
||||
md: [{ span: 12 }, { span: 0 }],
|
||||
sm: [{ span: 12 }, { span: 0 }],
|
||||
xs: [{ span: 12 }, { span: 0 }],
|
||||
xl: [{ span: 12 }, { span: 0 }],
|
||||
},
|
||||
default: {
|
||||
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 }],
|
||||
},
|
||||
};
|
||||
|
||||
return isUnitLibraryType ? layouts.fullWidth : layouts.default;
|
||||
}, [unitCategory])
|
||||
);
|
||||
|
||||
/**
|
||||
* Custom hook that restores the scroll position from `localStorage` after a page reload.
|
||||
* It listens for a `plugin.resize` message event and scrolls the window to the saved position
|
||||
|
||||
@@ -1,62 +1,10 @@
|
||||
import React from 'react';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { useScrollToLastPosition, useLayoutGrid } from './hooks';
|
||||
import { useScrollToLastPosition } from './hooks';
|
||||
import { iframeMessageTypes } from '../constants';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('useLayoutGrid', () => {
|
||||
it('returns fullWidth layout when isUnitLibraryType is true', () => {
|
||||
const { result } = renderHook(() => useLayoutGrid('someCategory', true));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
lg: [{ span: 12 }, { span: 0 }],
|
||||
md: [{ span: 12 }, { span: 0 }],
|
||||
sm: [{ span: 12 }, { span: 0 }],
|
||||
xs: [{ span: 12 }, { span: 0 }],
|
||||
xl: [{ span: 12 }, { span: 0 }],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns default layout when isUnitLibraryType is false', () => {
|
||||
const { result } = renderHook(() => useLayoutGrid('someCategory', false));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
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 }],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not recompute layout if unitCategory remains the same', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ unitCategory, isUnitLibraryType }) => useLayoutGrid(unitCategory, isUnitLibraryType),
|
||||
{ initialProps: { unitCategory: 'category1', isUnitLibraryType: false } },
|
||||
);
|
||||
|
||||
const firstResult = result.current;
|
||||
|
||||
rerender({ unitCategory: 'category1', isUnitLibraryType: false });
|
||||
|
||||
expect(result.current).toBe(firstResult);
|
||||
});
|
||||
|
||||
it('recomputes layout when unitCategory changes', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ unitCategory, isUnitLibraryType }) => useLayoutGrid(unitCategory, isUnitLibraryType),
|
||||
{ initialProps: { unitCategory: 'category1', isUnitLibraryType: false } },
|
||||
);
|
||||
|
||||
const firstResult = result.current;
|
||||
|
||||
rerender({ unitCategory: 'category2', isUnitLibraryType: false });
|
||||
|
||||
expect(result.current).not.toBe(firstResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useScrollToLastPosition', () => {
|
||||
const storageKey = 'createXBlockLastYPosition';
|
||||
let scrollToSpy;
|
||||
|
||||
@@ -21,13 +21,6 @@
|
||||
padding: 0 var(--pgn-spacing-spacer-base) var(--pgn-spacing-spacer-base);
|
||||
|
||||
.course-unit-sidebar-visibility {
|
||||
.course-unit-sidebar-visibility-title {
|
||||
font-weight: var(--pgn-typography-font-weight-normal);
|
||||
color: var(--pgn-color-gray-700);
|
||||
|
||||
@extend %base-font-params;
|
||||
}
|
||||
|
||||
.course-unit-sidebar-visibility-section {
|
||||
@extend %base-font-params;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { Card } from '@openedx/paragon';
|
||||
|
||||
const Sidebar = ({ className = null, children = null, ...props }:SidebarProps) => (
|
||||
const SidebarSection = ({ className = null, children = null, ...props }:SidebarSectionProps) => (
|
||||
<Card
|
||||
className={classNames('course-unit-sidebar', className)}
|
||||
{...props}
|
||||
@@ -10,9 +10,9 @@ const Sidebar = ({ className = null, children = null, ...props }:SidebarProps) =
|
||||
</Card>
|
||||
);
|
||||
|
||||
interface SidebarProps {
|
||||
interface SidebarSectionProps {
|
||||
className?: string | null;
|
||||
children?: React.ReactNode | null;
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
export default SidebarSection;
|
||||
@@ -13,7 +13,7 @@ const SidebarHeader = ({ title, visibilityState, displayUnitLocation }) => {
|
||||
const { iconSrc, colorVariant } = getIconVariant(visibilityState, published, hasChanges);
|
||||
|
||||
return (
|
||||
<Stack className="course-unit-sidebar-header" direction="horizontal">
|
||||
<Stack className="course-unit-sidebar-header" direction="horizontal" gap={2}>
|
||||
{!displayUnitLocation && (
|
||||
<Icon
|
||||
className="course-unit-sidebar-header-icon"
|
||||
@@ -2,19 +2,21 @@ import { useSelector } from 'react-redux';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Divider } from '../../../../generic/divider';
|
||||
import { getCanEdit, getCourseUnitData } from '../../../data/selectors';
|
||||
import { useClipboard } from '../../../../generic/clipboard';
|
||||
import { Divider } from '@src/generic/divider';
|
||||
import { getCanEdit, getCourseUnitData } from '@src/course-unit/data/selectors';
|
||||
import { useClipboard } from '@src/generic/clipboard';
|
||||
import messages from '../../messages';
|
||||
|
||||
interface ActionButtonsProps {
|
||||
openDiscardModal: () => void,
|
||||
handlePublishing: () => void,
|
||||
hideCopyButton?: boolean,
|
||||
}
|
||||
|
||||
const ActionButtons = ({
|
||||
openDiscardModal,
|
||||
handlePublishing,
|
||||
hideCopyButton = false,
|
||||
}: ActionButtonsProps) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
@@ -32,7 +34,7 @@ const ActionButtons = ({
|
||||
<Button
|
||||
size="sm"
|
||||
className="mt-3.5"
|
||||
variant="outline-primary"
|
||||
variant="primary"
|
||||
onClick={handlePublishing}
|
||||
>
|
||||
{intl.formatMessage(messages.actionButtonPublishTitle)}
|
||||
@@ -48,7 +50,7 @@ const ActionButtons = ({
|
||||
{intl.formatMessage(messages.actionButtonDiscardChangesTitle)}
|
||||
</Button>
|
||||
)}
|
||||
{enableCopyPasteUnits && canEdit && (
|
||||
{enableCopyPasteUnits && canEdit && !hideCopyButton && (
|
||||
<>
|
||||
<Divider className="course-unit-sidebar-footer__divider" />
|
||||
<Button
|
||||
@@ -2,25 +2,22 @@ import { Card, Stack } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../../messages';
|
||||
import UnitVisibilityInfo from './UnitVisibilityInfo';
|
||||
import ActionButtons from './ActionButtons';
|
||||
|
||||
interface SidebarFooterProps {
|
||||
locationId?: string,
|
||||
displayUnitLocation?: boolean,
|
||||
openDiscardModal: () => void,
|
||||
openVisibleModal: () => void,
|
||||
handlePublishing: () => void,
|
||||
visibleToStaffOnly: boolean,
|
||||
hideCopyButton?: boolean,
|
||||
}
|
||||
|
||||
const SidebarFooter = ({
|
||||
locationId,
|
||||
openVisibleModal,
|
||||
handlePublishing,
|
||||
openDiscardModal,
|
||||
visibleToStaffOnly,
|
||||
displayUnitLocation = false,
|
||||
hideCopyButton = false,
|
||||
}: SidebarFooterProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -32,16 +29,11 @@ const SidebarFooter = ({
|
||||
{intl.formatMessage(messages.unitLocationDescription, { id: locationId })}
|
||||
</small>
|
||||
) : (
|
||||
<>
|
||||
<UnitVisibilityInfo
|
||||
openVisibleModal={openVisibleModal}
|
||||
visibleToStaffOnly={visibleToStaffOnly}
|
||||
/>
|
||||
<ActionButtons
|
||||
openDiscardModal={openDiscardModal}
|
||||
handlePublishing={handlePublishing}
|
||||
/>
|
||||
</>
|
||||
<ActionButtons
|
||||
openDiscardModal={openDiscardModal}
|
||||
handlePublishing={handlePublishing}
|
||||
hideCopyButton={hideCopyButton}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Card.Footer>
|
||||
@@ -21,6 +21,15 @@ const useCourseUnitData = ({
|
||||
: messages.sidebarTitleDraftNeverPublished,
|
||||
};
|
||||
|
||||
const cardClasses = {
|
||||
[UNIT_VISIBILITY_STATES.staffOnly]: 'only-staff',
|
||||
[UNIT_VISIBILITY_STATES.live]: 'live',
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
default: published
|
||||
? (hasChanges ? 'draft' : 'published')
|
||||
: 'draft',
|
||||
};
|
||||
|
||||
const releaseLabels = {
|
||||
[UNIT_VISIBILITY_STATES.staffOnly]: releaseStatus.release,
|
||||
[UNIT_VISIBILITY_STATES.live]: releaseStatus.released,
|
||||
@@ -30,6 +39,7 @@ const useCourseUnitData = ({
|
||||
|
||||
const title = intl.formatMessage(titleMessages[visibilityState] || titleMessages.default);
|
||||
const releaseLabel = releaseLabels[visibilityState] || releaseLabels.default;
|
||||
const publishCardClass = cardClasses[visibilityState] || cardClasses.default;
|
||||
|
||||
return {
|
||||
title,
|
||||
@@ -37,6 +47,7 @@ const useCourseUnitData = ({
|
||||
releaseLabel,
|
||||
visibilityState,
|
||||
visibleToStaffOnly,
|
||||
publishCardClass,
|
||||
};
|
||||
};
|
||||
|
||||
78
src/course-unit/legacy-sidebar/index.tsx
Normal file
78
src/course-unit/legacy-sidebar/index.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useParams } from 'react-router';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework/dist';
|
||||
import { Stack } from '@openedx/paragon';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import TagsSidebarControls from '@src/content-tags-drawer/tags-sidebar-controls';
|
||||
import SidebarSection from './SidebarSection';
|
||||
import LocationInfo from './LocationInfo';
|
||||
import SplitTestSidebarInfo from './SplitTestSidebarInfo';
|
||||
import PublishControls from '../unit-sidebar/unit-info/PublishControls';
|
||||
|
||||
export type XBlock = {
|
||||
id: string,
|
||||
name: string,
|
||||
blockType: string,
|
||||
};
|
||||
|
||||
export interface LegacySidebarProps {
|
||||
unitTitle: string;
|
||||
xBlocks: XBlock[];
|
||||
readOnly: boolean;
|
||||
isUnitVerticalType: boolean;
|
||||
isSplitTestType: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sidebar that renders the unit details.
|
||||
*
|
||||
* This is an old sidebar replaced by src/course-unit/unit-sidebar/unit-info/UnitInfoSidebar.tsx
|
||||
* When the new sidebar is active by default, this sidebar will be removed.
|
||||
*/
|
||||
const LegacySidebar = ({
|
||||
unitTitle,
|
||||
isUnitVerticalType,
|
||||
xBlocks,
|
||||
readOnly,
|
||||
isSplitTestType,
|
||||
}: LegacySidebarProps) => {
|
||||
const { blockId } = useParams();
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
|
||||
return (
|
||||
<Stack gap={3} className="px-4 mw-sm">
|
||||
{isUnitVerticalType && (
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.authoring.course_unit_sidebar.v1"
|
||||
idAliases={['course_authoring_unit_sidebar_slot']}
|
||||
pluginProps={{
|
||||
blockId,
|
||||
courseId,
|
||||
unitTitle,
|
||||
xBlocks,
|
||||
readOnly,
|
||||
}}
|
||||
>
|
||||
<SidebarSection data-testid="course-unit-sidebar">
|
||||
<PublishControls blockId={blockId} />
|
||||
</SidebarSection>
|
||||
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
|
||||
<SidebarSection className="tags-sidebar">
|
||||
<TagsSidebarControls readOnly={readOnly} />
|
||||
</SidebarSection>
|
||||
)}
|
||||
<SidebarSection data-testid="course-unit-location-sidebar">
|
||||
<LocationInfo />
|
||||
</SidebarSection>
|
||||
</PluginSlot>
|
||||
)}
|
||||
{isSplitTestType && (
|
||||
<SidebarSection data-testid="course-split-test-sidebar">
|
||||
<SplitTestSidebarInfo />
|
||||
</SidebarSection>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegacySidebar;
|
||||
@@ -33,14 +33,6 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-unit.publish.info.previously-published',
|
||||
defaultMessage: 'Previously published',
|
||||
},
|
||||
publishInfoDraftSaved: {
|
||||
id: 'course-authoring.course-unit.publish.info.draft.saved',
|
||||
defaultMessage: 'Draft saved on {editedOn} by {editedBy}',
|
||||
},
|
||||
publishLastPublished: {
|
||||
id: 'course-authoring.course-unit.publish.info.last.published',
|
||||
defaultMessage: 'Last published {publishedOn} by {publishedBy}',
|
||||
},
|
||||
releaseInfoUnscheduled: {
|
||||
id: 'course-authoring.course-unit.release.info.unscheduled',
|
||||
defaultMessage: 'Unscheduled',
|
||||
@@ -65,22 +57,6 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-unit.unit-location.description',
|
||||
defaultMessage: 'To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value',
|
||||
},
|
||||
visibilityCheckboxTitle: {
|
||||
id: 'course-authoring.course-unit.visibility.checkbox.title',
|
||||
defaultMessage: 'Hide from learners',
|
||||
},
|
||||
visibilityStaffOnlyTitle: {
|
||||
id: 'course-authoring.course-unit.visibility.staff-only.title',
|
||||
defaultMessage: 'Staff only',
|
||||
},
|
||||
visibilityStaffAndLearnersTitle: {
|
||||
id: 'course-authoring.course-unit.visibility.staff-and-learners.title',
|
||||
defaultMessage: 'Staff and learners',
|
||||
},
|
||||
visibilityHasExplicitStaffLockText: {
|
||||
id: 'course-authoring.course-unit.visibility.has-explicit-staff-lock.text',
|
||||
defaultMessage: 'with {sectionName}',
|
||||
},
|
||||
actionButtonPublishTitle: {
|
||||
id: 'course-authoring.course-unit.action-buttons.publish.title',
|
||||
defaultMessage: 'Publish',
|
||||
@@ -105,38 +81,6 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-unit.status.scheduled.title',
|
||||
defaultMessage: 'SCHEDULED',
|
||||
},
|
||||
modalDiscardUnitChangesTitle: {
|
||||
id: 'course-authoring.course-unit.modal.discard-unit-changes.title',
|
||||
defaultMessage: 'Discard changes',
|
||||
},
|
||||
modalDiscardUnitChangesActionButtonText: {
|
||||
id: 'course-authoring.course-unit.modal.discard-unit-changes.btn.action.text',
|
||||
defaultMessage: 'Discard changes',
|
||||
},
|
||||
modalDiscardUnitChangesCancelButtonText: {
|
||||
id: 'course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
modalDiscardUnitChangesDescription: {
|
||||
id: 'course-authoring.course-unit.modal.discard-unit-changes.description',
|
||||
defaultMessage: 'Are you sure you want to revert to the last published version of the unit? You cannot undo this action.',
|
||||
},
|
||||
modalMakeVisibilityTitle: {
|
||||
id: 'course-authoring.course-unit.modal.make-visibility.title',
|
||||
defaultMessage: 'Make visible to students',
|
||||
},
|
||||
modalMakeVisibilityActionButtonText: {
|
||||
id: 'course-authoring.course-unit.modal.make-visibility.btn.action.text',
|
||||
defaultMessage: 'Make visible to students',
|
||||
},
|
||||
modalMakeVisibilityCancelButtonText: {
|
||||
id: 'course-authoring.course-unit.modal.make-visibility.btn.cancel.text',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
modalMakeVisibilityDescription: {
|
||||
id: 'course-authoring.course-unit.modal.make-visibility.description',
|
||||
defaultMessage: 'If the unit was previously published and released to students, any changes you made to the unit when it was hidden will now be visible to students. Do you want to proceed?',
|
||||
},
|
||||
sidebarSplitTestAddComponentTitle: {
|
||||
id: 'course-authoring.course-unit.split-test.sidebar.add-component.title',
|
||||
defaultMessage: 'Adding components',
|
||||
@@ -1,7 +1,8 @@
|
||||
import {
|
||||
AccessTimeFilled,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
CheckCircleOutline as CheckCircleOutlineIcon,
|
||||
InfoOutline as InfoOutlineIcon,
|
||||
Description,
|
||||
Lock,
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import { ICON_COLOR_VARIANTS, UNIT_VISIBILITY_STATES } from '../constants';
|
||||
@@ -52,22 +53,6 @@ export const getReleaseInfo = (intl, releaseDate, releaseDateFrom) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the visibility title.
|
||||
* @param {Object} intl - The internationalization object.
|
||||
* @param {boolean} releasedToStudents - Indicates if the content is released to students.
|
||||
* @param {boolean} published - Indicates if the content is published.
|
||||
* @param {boolean} hasChanges - Indicates if there are unpublished changes.
|
||||
* @returns {string} The visibility title determined by the provided parameters.
|
||||
*/
|
||||
export const getVisibilityTitle = (intl, releasedToStudents, published, hasChanges) => {
|
||||
if (releasedToStudents && published && !hasChanges) {
|
||||
return intl.formatMessage(messages.visibilityIsVisibleToTitle);
|
||||
}
|
||||
|
||||
return intl.formatMessage(messages.visibilityWillBeVisibleToTitle);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the icon variant based on the provided visibility state and publication status.
|
||||
* @param {string} visibilityState - The visibility state of the content.
|
||||
@@ -79,11 +64,11 @@ export const getVisibilityTitle = (intl, releasedToStudents, published, hasChang
|
||||
*/
|
||||
export const getIconVariant = (visibilityState, published, hasChanges) => {
|
||||
const iconVariants = {
|
||||
[UNIT_VISIBILITY_STATES.staffOnly]: { iconSrc: InfoOutlineIcon, colorVariant: ICON_COLOR_VARIANTS.BLACK },
|
||||
[UNIT_VISIBILITY_STATES.staffOnly]: { iconSrc: Lock, colorVariant: ICON_COLOR_VARIANTS.PRIMARY },
|
||||
[UNIT_VISIBILITY_STATES.live]: { iconSrc: CheckCircleIcon, colorVariant: ICON_COLOR_VARIANTS.GREEN },
|
||||
publishedNoChanges: { iconSrc: CheckCircleOutlineIcon, colorVariant: ICON_COLOR_VARIANTS.BLACK },
|
||||
publishedWithChanges: { iconSrc: InfoOutlineIcon, colorVariant: ICON_COLOR_VARIANTS.BLACK },
|
||||
default: { iconSrc: InfoOutlineIcon, colorVariant: ICON_COLOR_VARIANTS.BLACK },
|
||||
publishedNoChanges: { iconSrc: AccessTimeFilled, colorVariant: ICON_COLOR_VARIANTS.INFO },
|
||||
publishedWithChanges: { iconSrc: Description, colorVariant: ICON_COLOR_VARIANTS.ORANGE },
|
||||
default: { iconSrc: Description, colorVariant: ICON_COLOR_VARIANTS.ORANGE },
|
||||
};
|
||||
if (visibilityState in iconVariants) {
|
||||
return iconVariants[visibilityState];
|
||||
@@ -1,97 +0,0 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
import { InfoOutline as InfoOutlineIcon } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import useCourseUnitData from './hooks';
|
||||
import { useIframe } from '../../generic/hooks/context/hooks';
|
||||
import { editCourseUnitVisibilityAndData } from '../data/thunk';
|
||||
import { SidebarBody, SidebarFooter, SidebarHeader } from './components';
|
||||
import { PUBLISH_TYPES, messageTypes } from '../constants';
|
||||
import { getCourseUnitData } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
import ModalNotification from '../../generic/modal-notification';
|
||||
|
||||
interface PublishControlsProps {
|
||||
blockId?: string,
|
||||
}
|
||||
|
||||
const PublishControls = ({ blockId }: PublishControlsProps) => {
|
||||
const unitData = useSelector(getCourseUnitData);
|
||||
const {
|
||||
title,
|
||||
locationId,
|
||||
releaseLabel,
|
||||
visibilityState,
|
||||
visibleToStaffOnly,
|
||||
} = useCourseUnitData(unitData);
|
||||
const intl = useIntl();
|
||||
const { sendMessageToIframe } = useIframe();
|
||||
|
||||
const [isDiscardModalOpen, openDiscardModal, closeDiscardModal] = useToggle(false);
|
||||
const [isVisibleModalOpen, openVisibleModal, closeVisibleModal] = useToggle(false);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleCourseUnitVisibility = () => {
|
||||
closeVisibleModal();
|
||||
dispatch(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, null));
|
||||
};
|
||||
|
||||
const handleCourseUnitDiscardChanges = () => {
|
||||
closeDiscardModal();
|
||||
dispatch(editCourseUnitVisibilityAndData(
|
||||
blockId,
|
||||
PUBLISH_TYPES.discardChanges,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
() => sendMessageToIframe(messageTypes.refreshXBlock, null),
|
||||
));
|
||||
};
|
||||
|
||||
const handleCourseUnitPublish = () => {
|
||||
dispatch(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarHeader
|
||||
title={title}
|
||||
visibilityState={visibilityState}
|
||||
/>
|
||||
<SidebarBody
|
||||
releaseLabel={releaseLabel}
|
||||
visibleToStaffOnly={visibleToStaffOnly}
|
||||
/>
|
||||
<SidebarFooter
|
||||
locationId={locationId}
|
||||
openDiscardModal={openDiscardModal}
|
||||
openVisibleModal={openVisibleModal}
|
||||
handlePublishing={handleCourseUnitPublish}
|
||||
visibleToStaffOnly={visibleToStaffOnly}
|
||||
/>
|
||||
<ModalNotification
|
||||
title={intl.formatMessage(messages.modalDiscardUnitChangesTitle)}
|
||||
isOpen={isDiscardModalOpen}
|
||||
actionButtonText={intl.formatMessage(messages.modalDiscardUnitChangesActionButtonText)}
|
||||
cancelButtonText={intl.formatMessage(messages.modalDiscardUnitChangesCancelButtonText)}
|
||||
handleAction={handleCourseUnitDiscardChanges}
|
||||
handleCancel={closeDiscardModal}
|
||||
message={intl.formatMessage(messages.modalDiscardUnitChangesDescription)}
|
||||
icon={InfoOutlineIcon}
|
||||
/>
|
||||
<ModalNotification
|
||||
title={intl.formatMessage(messages.modalMakeVisibilityTitle)}
|
||||
isOpen={isVisibleModalOpen}
|
||||
actionButtonText={intl.formatMessage(messages.modalMakeVisibilityActionButtonText)}
|
||||
cancelButtonText={intl.formatMessage(messages.modalMakeVisibilityCancelButtonText)}
|
||||
handleAction={handleCourseUnitVisibility}
|
||||
handleCancel={closeVisibleModal}
|
||||
message={intl.formatMessage(messages.modalMakeVisibilityDescription)}
|
||||
icon={InfoOutlineIcon}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublishControls;
|
||||
@@ -1,69 +0,0 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Form } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { getCourseUnitData } from '../../../data/selectors';
|
||||
import { editCourseUnitVisibilityAndData } from '../../../data/thunk';
|
||||
import { PUBLISH_TYPES } from '../../../constants';
|
||||
import { getVisibilityTitle } from '../../utils';
|
||||
import messages from '../../messages';
|
||||
|
||||
interface UnitVisibilityInfoProps {
|
||||
openVisibleModal: () => void,
|
||||
visibleToStaffOnly: boolean,
|
||||
}
|
||||
|
||||
const UnitVisibilityInfo = ({
|
||||
openVisibleModal,
|
||||
visibleToStaffOnly,
|
||||
}: UnitVisibilityInfoProps) => {
|
||||
const intl = useIntl();
|
||||
const { blockId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
published,
|
||||
hasChanges,
|
||||
staffLockFrom,
|
||||
releasedToStudents,
|
||||
hasExplicitStaffLock,
|
||||
} = useSelector(getCourseUnitData);
|
||||
|
||||
const handleCourseUnitVisibility = () => {
|
||||
dispatch(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, true));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<small className="course-unit-sidebar-visibility-title">
|
||||
{getVisibilityTitle(intl, releasedToStudents, published, hasChanges)}
|
||||
</small>
|
||||
{visibleToStaffOnly ? (
|
||||
<>
|
||||
<h6 className="course-unit-sidebar-visibility-copy">
|
||||
{intl.formatMessage(messages.visibilityStaffOnlyTitle)}
|
||||
</h6>
|
||||
{!hasExplicitStaffLock && (
|
||||
<span className="course-unit-sidebar-visibility-section mb-2">
|
||||
{intl.formatMessage(messages.visibilityHasExplicitStaffLockText, { sectionName: staffLockFrom })}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<h6 className="course-unit-sidebar-visibility-copy">
|
||||
{intl.formatMessage(messages.visibilityStaffAndLearnersTitle)}
|
||||
</h6>
|
||||
)}
|
||||
<Form.Checkbox
|
||||
className="course-unit-sidebar-visibility-checkbox"
|
||||
checked={hasExplicitStaffLock}
|
||||
onChange={hasExplicitStaffLock ? null : handleCourseUnitVisibility}
|
||||
onClick={hasExplicitStaffLock ? openVisibleModal : null}
|
||||
>
|
||||
{intl.formatMessage(messages.visibilityCheckboxTitle)}
|
||||
</Form.Checkbox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnitVisibilityInfo;
|
||||
36
src/course-unit/unit-sidebar/UnitSidebar.tsx
Normal file
36
src/course-unit/unit-sidebar/UnitSidebar.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Sidebar } from '@src/generic/sidebar';
|
||||
import LegacySidebar, { LegacySidebarProps } from '../legacy-sidebar';
|
||||
import { useUnitSidebarContext } from './UnitSidebarContext';
|
||||
import { isUnitPageNewDesignEnabled } from '../utils';
|
||||
import { UNIT_SIDEBAR_PAGES } from './constants';
|
||||
|
||||
export type UnitSidebarProps = {
|
||||
legacySidebarProps: LegacySidebarProps,
|
||||
};
|
||||
|
||||
export const UnitSidebar = ({
|
||||
legacySidebarProps, // Can be deleted when the legacy sidebar is deprecated
|
||||
}: UnitSidebarProps) => {
|
||||
const {
|
||||
currentPageKey,
|
||||
setCurrentPageKey,
|
||||
isOpen,
|
||||
toggle,
|
||||
} = useUnitSidebarContext();
|
||||
|
||||
if (!isUnitPageNewDesignEnabled()) {
|
||||
return (
|
||||
<LegacySidebar {...legacySidebarProps} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
pages={UNIT_SIDEBAR_PAGES}
|
||||
currentPageKey={currentPageKey}
|
||||
setCurrentPageKey={setCurrentPageKey}
|
||||
isOpen={isOpen}
|
||||
toggle={toggle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
67
src/course-unit/unit-sidebar/UnitSidebarContext.tsx
Normal file
67
src/course-unit/unit-sidebar/UnitSidebarContext.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
createContext, useCallback, useContext, useMemo, useState,
|
||||
} from 'react';
|
||||
import { SidebarPage } from '@src/generic/sidebar';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
|
||||
export type UnitSidebarPageKeys = 'info';
|
||||
export type UnitSidebarPages = Record<UnitSidebarPageKeys, SidebarPage>;
|
||||
|
||||
interface UnitSidebarContextData {
|
||||
currentPageKey: UnitSidebarPageKeys;
|
||||
setCurrentPageKey: (pageKey: UnitSidebarPageKeys) => void;
|
||||
currentTabKey?: string;
|
||||
setCurrentTabKey: (tabKey: string) => void;
|
||||
isOpen: boolean;
|
||||
open: () => void;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
const UnitSidebarContext = createContext<UnitSidebarContextData | undefined>(undefined);
|
||||
|
||||
export const UnitSidebarProvider = ({ children }: { children?: React.ReactNode }) => {
|
||||
const [currentPageKey, setCurrentPageKeyState] = useState<UnitSidebarPageKeys>('info');
|
||||
const [currentTabKey, setCurrentTabKey] = useState<string>();
|
||||
const [isOpen, open,, toggle] = useToggle(true);
|
||||
|
||||
const setCurrentPageKey = useCallback(/* istanbul ignore next */ (pageKey: UnitSidebarPageKeys) => {
|
||||
setCurrentPageKeyState(pageKey);
|
||||
open();
|
||||
}, [open]);
|
||||
|
||||
const context = useMemo<UnitSidebarContextData>(
|
||||
() => ({
|
||||
currentPageKey,
|
||||
setCurrentPageKey,
|
||||
currentTabKey,
|
||||
setCurrentTabKey,
|
||||
isOpen,
|
||||
open,
|
||||
toggle,
|
||||
}),
|
||||
[
|
||||
currentPageKey,
|
||||
setCurrentPageKey,
|
||||
currentTabKey,
|
||||
setCurrentTabKey,
|
||||
isOpen,
|
||||
open,
|
||||
toggle,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<UnitSidebarContext.Provider value={context}>
|
||||
{children}
|
||||
</UnitSidebarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useUnitSidebarContext(): UnitSidebarContextData {
|
||||
const ctx = useContext(UnitSidebarContext);
|
||||
if (ctx === undefined) {
|
||||
/* istanbul ignore next */
|
||||
throw new Error('useUnitSidebarContext() was used in a component without a <UnitSidebarProvider> ancestor.');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
20
src/course-unit/unit-sidebar/constants.ts
Normal file
20
src/course-unit/unit-sidebar/constants.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Info } from '@openedx/paragon/icons';
|
||||
import { SidebarPage } from '@src/generic/sidebar';
|
||||
import messages from './messages';
|
||||
import { UnitInfoSidebar } from './unit-info/UnitInfoSidebar';
|
||||
|
||||
export type UnitSidebarPageKeys = 'info';
|
||||
|
||||
/**
|
||||
* Sidebar pages for the unit sidebar
|
||||
*
|
||||
* This has been separated from the context to avoid a cyclical import
|
||||
* if you want to use the context in the sidebar pages.
|
||||
*/
|
||||
export const UNIT_SIDEBAR_PAGES: Record<UnitSidebarPageKeys, SidebarPage> = {
|
||||
info: {
|
||||
component: UnitInfoSidebar,
|
||||
icon: Info,
|
||||
title: messages.sidebarButtonInfo,
|
||||
},
|
||||
};
|
||||
11
src/course-unit/unit-sidebar/messages.ts
Normal file
11
src/course-unit/unit-sidebar/messages.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
sidebarButtonInfo: {
|
||||
id: 'course-authoring.unit-page.sidebar.info.sidebar-button-info',
|
||||
defaultMessage: 'Info',
|
||||
description: 'Label of the button for the Info sidebar',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
19
src/course-unit/unit-sidebar/unit-info/PublishControls.scss
Normal file
19
src/course-unit/unit-sidebar/unit-info/PublishControls.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
.course-unit-publish-controls {
|
||||
border-radius: 5px;
|
||||
|
||||
&.draft {
|
||||
box-shadow: 0 -7px 0 0 #B4610E;
|
||||
}
|
||||
|
||||
&.only-staff {
|
||||
box-shadow: 0 -7px 0 0 var(--pgn-color-primary-700);
|
||||
}
|
||||
|
||||
&.live {
|
||||
box-shadow: 0 -7px 0 0 var(--pgn-color-success-500);
|
||||
}
|
||||
|
||||
&.published {
|
||||
box-shadow: 0 -7px 0 0 var(--pgn-color-info-500);
|
||||
}
|
||||
}
|
||||
174
src/course-unit/unit-sidebar/unit-info/PublishControls.tsx
Normal file
174
src/course-unit/unit-sidebar/unit-info/PublishControls.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Icon, Stack, useToggle } from '@openedx/paragon';
|
||||
import { InfoOutline as InfoOutlineIcon, Person } from '@openedx/paragon/icons';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import ModalNotification from '@src/generic/modal-notification';
|
||||
import { useIframe } from '@src/generic/hooks/context/hooks';
|
||||
import { getCourseUnitData } from '@src/course-unit/data/selectors';
|
||||
import { editCourseUnitVisibilityAndData } from '@src/course-unit/data/thunk';
|
||||
import { messageTypes, PUBLISH_TYPES } from '@src/course-unit/constants';
|
||||
import { SidebarFooter, SidebarHeader } from '@src/course-unit/legacy-sidebar/components';
|
||||
import useCourseUnitData from '@src/course-unit/legacy-sidebar/hooks';
|
||||
import ReleaseInfoComponent from '@src/course-unit/legacy-sidebar/components/ReleaseInfoComponent';
|
||||
import messages from './messages';
|
||||
import UnitVisibilityInfo from './UnitVisibilityInfo';
|
||||
|
||||
interface PublishControlsProps {
|
||||
blockId?: string,
|
||||
hideCopyButton?: boolean,
|
||||
}
|
||||
|
||||
const PublishControls = ({
|
||||
blockId,
|
||||
hideCopyButton = false,
|
||||
}: PublishControlsProps) => {
|
||||
const unitData = useSelector(getCourseUnitData);
|
||||
const {
|
||||
title,
|
||||
locationId,
|
||||
releaseLabel,
|
||||
visibilityState,
|
||||
visibleToStaffOnly,
|
||||
publishCardClass,
|
||||
} = useCourseUnitData(unitData);
|
||||
const intl = useIntl();
|
||||
const { sendMessageToIframe } = useIframe();
|
||||
|
||||
const [isDiscardModalOpen, openDiscardModal, closeDiscardModal] = useToggle(false);
|
||||
const [isVisibleModalOpen, openVisibleModal, closeVisibleModal] = useToggle(false);
|
||||
|
||||
const {
|
||||
editedOn,
|
||||
editedBy,
|
||||
publishedBy,
|
||||
publishedOn,
|
||||
} = unitData;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleCourseUnitVisibility = () => {
|
||||
closeVisibleModal();
|
||||
dispatch(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, null));
|
||||
};
|
||||
|
||||
const handleCourseUnitDiscardChanges = () => {
|
||||
closeDiscardModal();
|
||||
dispatch(editCourseUnitVisibilityAndData(
|
||||
blockId,
|
||||
PUBLISH_TYPES.discardChanges,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
/* istanbul ignore next */
|
||||
() => sendMessageToIframe(messageTypes.refreshXBlock, null),
|
||||
));
|
||||
};
|
||||
|
||||
const handleCourseUnitPublish = () => {
|
||||
dispatch(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`course-unit-publish-controls border p-3 ${publishCardClass}`}>
|
||||
<div className="text-primary-700 mb-4">
|
||||
<SidebarHeader
|
||||
title={title}
|
||||
visibilityState={visibilityState}
|
||||
/>
|
||||
</div>
|
||||
<Stack gap={4}>
|
||||
<Stack gap={2}>
|
||||
{editedOn && (
|
||||
<div>
|
||||
<span className="heading-label">
|
||||
<FormattedMessage {...messages.publishInfoDraftSaved} />
|
||||
</span>
|
||||
<Stack direction="horizontal" gap={1} className="text-primary-700">
|
||||
{editedBy && (
|
||||
<>
|
||||
<Icon src={Person} />
|
||||
<span>
|
||||
{editedBy}
|
||||
</span>
|
||||
<span>
|
||||
-
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span>
|
||||
{editedOn}
|
||||
</span>
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
{publishedOn && (
|
||||
<div>
|
||||
<span className="heading-label">
|
||||
<FormattedMessage {...messages.publishLastPublished} />
|
||||
</span>
|
||||
<Stack direction="horizontal" gap={1} className="text-primary-700">
|
||||
{publishedBy && (
|
||||
<>
|
||||
<Icon src={Person} />
|
||||
<span>
|
||||
{publishedBy}
|
||||
</span>
|
||||
<span>
|
||||
-
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span>
|
||||
{publishedOn}
|
||||
</span>
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack>
|
||||
<span className="heading-label">
|
||||
{releaseLabel}
|
||||
</span>
|
||||
<div className="text-primary-700">
|
||||
<ReleaseInfoComponent />
|
||||
</div>
|
||||
</Stack>
|
||||
<div>
|
||||
<UnitVisibilityInfo
|
||||
openVisibleModal={openVisibleModal}
|
||||
visibleToStaffOnly={visibleToStaffOnly}
|
||||
userPartitionInfo={unitData.userPartitionInfo}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
<SidebarFooter
|
||||
locationId={locationId}
|
||||
openDiscardModal={openDiscardModal}
|
||||
handlePublishing={handleCourseUnitPublish}
|
||||
hideCopyButton={hideCopyButton}
|
||||
/>
|
||||
<ModalNotification
|
||||
title={intl.formatMessage(messages.modalDiscardUnitChangesTitle)}
|
||||
isOpen={isDiscardModalOpen}
|
||||
actionButtonText={intl.formatMessage(messages.modalDiscardUnitChangesActionButtonText)}
|
||||
cancelButtonText={intl.formatMessage(messages.modalDiscardUnitChangesCancelButtonText)}
|
||||
handleAction={handleCourseUnitDiscardChanges}
|
||||
handleCancel={closeDiscardModal}
|
||||
message={intl.formatMessage(messages.modalDiscardUnitChangesDescription)}
|
||||
icon={InfoOutlineIcon}
|
||||
/>
|
||||
<ModalNotification
|
||||
title={intl.formatMessage(messages.modalMakeVisibilityTitle)}
|
||||
isOpen={isVisibleModalOpen}
|
||||
actionButtonText={intl.formatMessage(messages.modalMakeVisibilityActionButtonText)}
|
||||
cancelButtonText={intl.formatMessage(messages.modalMakeVisibilityCancelButtonText)}
|
||||
handleAction={handleCourseUnitVisibility}
|
||||
handleCancel={closeVisibleModal}
|
||||
message={intl.formatMessage(messages.modalMakeVisibilityDescription)}
|
||||
icon={InfoOutlineIcon}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublishControls;
|
||||
238
src/course-unit/unit-sidebar/unit-info/UnitInfoSidebar.tsx
Normal file
238
src/course-unit/unit-sidebar/unit-info/UnitInfoSidebar.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ComponentCountSnippet, getItemIcon } from '@src/generic/block-type-utils';
|
||||
import { SidebarContent, SidebarSection, SidebarTitle } from '@src/generic/sidebar';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { Tag } from '@openedx/paragon/icons';
|
||||
import { ContentTagsSnippet } from '@src/content-tags-drawer';
|
||||
import configureMessages from '@src/generic/configure-modal/messages';
|
||||
import {
|
||||
Button, ButtonGroup, Tab, Tabs,
|
||||
} from '@openedx/paragon';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useIframe } from '@src/generic/hooks/context/hooks';
|
||||
import { AccessEditComponent, DiscussionEditComponent } from '@src/generic/configure-modal/UnitTab';
|
||||
import { Form, Formik } from 'formik';
|
||||
import { getCourseUnitData, getCourseVerticalChildren } from '@src/course-unit/data/selectors';
|
||||
import { messageTypes, PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from '@src/course-unit/constants';
|
||||
import { editCourseUnitVisibilityAndData } from '@src/course-unit/data/thunk';
|
||||
import PublishControls from './PublishControls';
|
||||
import { useUnitSidebarContext } from '../UnitSidebarContext';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* Component to show unit details: Publish status, Component counts and Content Tags.
|
||||
*
|
||||
* It's using in the details tab of the unit info sidebar.
|
||||
*/
|
||||
const UnitInfoDetails = () => {
|
||||
const intl = useIntl();
|
||||
const { blockId } = useParams();
|
||||
const courseVerticalChildren = useSelector(getCourseVerticalChildren);
|
||||
|
||||
if (blockId === undefined) {
|
||||
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
|
||||
throw new Error('Error: route is missing blockId.');
|
||||
}
|
||||
|
||||
const componentData: Record<string, number> = useMemo(() => (
|
||||
// @ts-ignore
|
||||
courseVerticalChildren.children.reduce<Record<string, number>>(
|
||||
(acc, { blockType }) => {
|
||||
acc[blockType] = (acc[blockType] ?? 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
)
|
||||
), [courseVerticalChildren.children]);
|
||||
|
||||
return (
|
||||
<SidebarContent>
|
||||
<PublishControls blockId={blockId} hideCopyButton />
|
||||
<SidebarSection
|
||||
title={intl.formatMessage(messages.sidebarSectionSummary)}
|
||||
icon={getItemIcon('unit')}
|
||||
>
|
||||
{componentData && <ComponentCountSnippet componentData={componentData} />}
|
||||
</SidebarSection>
|
||||
<SidebarSection
|
||||
title={intl.formatMessage(messages.sidebarSectionTaxonomies)}
|
||||
icon={Tag}
|
||||
>
|
||||
<ContentTagsSnippet contentId={blockId} />
|
||||
</SidebarSection>
|
||||
</SidebarContent>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Component with forms to edit unit settings.
|
||||
*
|
||||
* It's using in the settings tab of the unit info sidebar.
|
||||
*/
|
||||
const UnitInfoSettings = () => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const { sendMessageToIframe } = useIframe();
|
||||
const {
|
||||
id,
|
||||
visibilityState,
|
||||
discussionEnabled,
|
||||
userPartitionInfo,
|
||||
} = useSelector(getCourseUnitData);
|
||||
|
||||
const visibleToStaffOnly = visibilityState === UNIT_VISIBILITY_STATES.staffOnly;
|
||||
|
||||
const handleUpdate = async (
|
||||
isVisible: boolean,
|
||||
groupAccess: Object | null,
|
||||
isDiscussionEnabled: boolean,
|
||||
) => {
|
||||
await dispatch(editCourseUnitVisibilityAndData(
|
||||
id,
|
||||
PUBLISH_TYPES.republish,
|
||||
isVisible,
|
||||
groupAccess,
|
||||
isDiscussionEnabled,
|
||||
() => sendMessageToIframe(messageTypes.completeManageXBlockAccess, { locator: id }),
|
||||
id,
|
||||
));
|
||||
};
|
||||
|
||||
/* istanbul ignore next */
|
||||
const handleSaveGroups = async (data, { resetForm }) => {
|
||||
const groupAccess = {};
|
||||
if (data.selectedPartitionIndex >= 0) {
|
||||
const partitionId = userPartitionInfo.selectablePartitions[data.selectedPartitionIndex].id;
|
||||
groupAccess[partitionId] = data.selectedGroups.map(g => parseInt(g, 10));
|
||||
}
|
||||
await handleUpdate(visibleToStaffOnly, groupAccess, discussionEnabled);
|
||||
resetForm({ values: data });
|
||||
};
|
||||
|
||||
/* istanbul ignore next */
|
||||
const getSelectedGroups = () => {
|
||||
if (userPartitionInfo?.selectedPartitionIndex >= 0) {
|
||||
return userPartitionInfo?.selectablePartitions[userPartitionInfo?.selectedPartitionIndex]
|
||||
?.groups
|
||||
.filter(({ selected }) => selected)
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
.map(({ id }) => `${id}`)
|
||||
|| [];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const initialValues = useMemo(() => (
|
||||
{
|
||||
selectedPartitionIndex: userPartitionInfo?.selectedPartitionIndex,
|
||||
selectedGroups: getSelectedGroups(),
|
||||
}
|
||||
), [userPartitionInfo]);
|
||||
|
||||
return (
|
||||
<SidebarContent>
|
||||
<SidebarSection
|
||||
title={intl.formatMessage(messages.sidebarInfoVisibilityTitle)}
|
||||
>
|
||||
<ButtonGroup toggle>
|
||||
<Button
|
||||
variant={visibleToStaffOnly ? 'outline-primary' : 'primary'}
|
||||
onClick={() => handleUpdate(false, null, discussionEnabled)}
|
||||
>
|
||||
<FormattedMessage {...messages.sidebarInfoVisibilityStudentLabel} />
|
||||
</Button>
|
||||
<Button
|
||||
variant={visibleToStaffOnly ? 'primary' : 'outline-primary'}
|
||||
onClick={() => handleUpdate(true, null, discussionEnabled)}
|
||||
>
|
||||
<FormattedMessage {...messages.sidebarInfoVisibilityStaffLabel} />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</SidebarSection>
|
||||
<SidebarSection
|
||||
title={intl.formatMessage(messages.sidebarInfoAccessTitle)}
|
||||
>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSaveGroups}
|
||||
>
|
||||
{({
|
||||
values, setFieldValue, dirty,
|
||||
}) => (
|
||||
<Form>
|
||||
<AccessEditComponent
|
||||
selectedPartitionIndex={values.selectedPartitionIndex}
|
||||
setFieldValue={setFieldValue}
|
||||
userPartitionInfo={userPartitionInfo}
|
||||
selectedGroups={values.selectedGroups}
|
||||
/>
|
||||
{dirty && (
|
||||
<Button className="mt-3" type="submit" variant="primary">
|
||||
<FormattedMessage {...messages.visibilitySaveGroupsButton} />
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</SidebarSection>
|
||||
<SidebarSection
|
||||
title={intl.formatMessage(configureMessages.discussionEnabledSectionTitle)}
|
||||
>
|
||||
<DiscussionEditComponent
|
||||
discussionEnabled={discussionEnabled}
|
||||
handleDiscussionChange={(e) => handleUpdate(visibleToStaffOnly, null, e.target.checked)}
|
||||
/>
|
||||
</SidebarSection>
|
||||
</SidebarContent>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Main component that renders the tabs of the info sidebar.
|
||||
*/
|
||||
export const UnitInfoSidebar = () => {
|
||||
const intl = useIntl();
|
||||
const currentItemData = useSelector(getCourseUnitData);
|
||||
const {
|
||||
currentTabKey,
|
||||
setCurrentTabKey,
|
||||
} = useUnitSidebarContext();
|
||||
|
||||
useEffect(() => {
|
||||
// Set default Tab key
|
||||
setCurrentTabKey('details');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SidebarTitle
|
||||
title={currentItemData.displayName}
|
||||
icon={getItemIcon('unit')}
|
||||
/>
|
||||
<Tabs
|
||||
id="unit-info-sidebar-tabs"
|
||||
className="my-2 d-flex justify-content-around"
|
||||
activeKey={currentTabKey}
|
||||
onSelect={setCurrentTabKey}
|
||||
>
|
||||
<Tab
|
||||
eventKey="details"
|
||||
title={intl.formatMessage(messages.sidebarInfoDetailsTab)}
|
||||
>
|
||||
<div className="mt-4">
|
||||
<UnitInfoDetails />
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="settings"
|
||||
title={intl.formatMessage(messages.sidebarInfoSettingsTab)}
|
||||
>
|
||||
<div className="mt-4">
|
||||
<UnitInfoSettings />
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
165
src/course-unit/unit-sidebar/unit-info/UnitVisibilityInfo.tsx
Normal file
165
src/course-unit/unit-sidebar/unit-info/UnitVisibilityInfo.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
Form, Icon, IconButton, Stack,
|
||||
} from '@openedx/paragon';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { getCourseUnitData } from '@src/course-unit/data/selectors';
|
||||
import { editCourseUnitVisibilityAndData } from '@src/course-unit/data/thunk';
|
||||
import { PUBLISH_TYPES } from '@src/course-unit/constants';
|
||||
import { isUnitPageNewDesignEnabled } from '@src/course-unit/utils';
|
||||
import { Edit, Groups, Lock } from '@openedx/paragon/icons';
|
||||
import messages from './messages';
|
||||
import { useUnitSidebarContext } from '../UnitSidebarContext';
|
||||
|
||||
interface UnitVisibilityInfoProps {
|
||||
openVisibleModal: () => void,
|
||||
visibleToStaffOnly: boolean,
|
||||
userPartitionInfo?: {
|
||||
selectablePartitions: Record<string, any>[],
|
||||
selectedGroupsLabel: string,
|
||||
selectedPartitionIndex: number,
|
||||
},
|
||||
}
|
||||
|
||||
interface UnitvisibilityInfoContentProps {
|
||||
visibleToStaffOnly: boolean,
|
||||
userPartitionInfo?: {
|
||||
selectablePartitions: Record<string, any>[],
|
||||
selectedGroupsLabel: string,
|
||||
selectedPartitionIndex: number,
|
||||
},
|
||||
}
|
||||
|
||||
const LegacyVisibilityInfo = ({
|
||||
visibleToStaffOnly,
|
||||
openVisibleModal,
|
||||
}: UnitVisibilityInfoProps) => {
|
||||
const {
|
||||
staffLockFrom,
|
||||
hasExplicitStaffLock,
|
||||
} = useSelector(getCourseUnitData);
|
||||
|
||||
const { blockId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleCourseUnitVisibility = () => {
|
||||
/* istanbul ignore next */
|
||||
dispatch(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, true));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{visibleToStaffOnly ? (
|
||||
<>
|
||||
<h6 className="course-unit-sidebar-visibility-copy">
|
||||
<FormattedMessage {...messages.visibilityStaffOnlyTitle} />
|
||||
</h6>
|
||||
{/* istanbul ignore next */ !hasExplicitStaffLock && (
|
||||
<span className="course-unit-sidebar-visibility-section mb-2">
|
||||
<FormattedMessage
|
||||
{...messages.visibilityHasExplicitStaffLockText}
|
||||
values={{ sectionName: staffLockFrom }}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<h6 className="course-unit-sidebar-visibility-copy">
|
||||
<FormattedMessage {...messages.visibilityStaffAndLearnersTitle} />
|
||||
</h6>
|
||||
)}
|
||||
<Form.Checkbox
|
||||
className="course-unit-sidebar-visibility-checkbox"
|
||||
checked={hasExplicitStaffLock}
|
||||
onChange={hasExplicitStaffLock ? null : handleCourseUnitVisibility}
|
||||
onClick={hasExplicitStaffLock ? openVisibleModal : null}
|
||||
>
|
||||
<FormattedMessage {...messages.visibilityCheckboxTitle} />
|
||||
</Form.Checkbox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UnitvisibilityInfoContent = ({
|
||||
visibleToStaffOnly,
|
||||
userPartitionInfo,
|
||||
}: UnitvisibilityInfoContentProps) => {
|
||||
const intl = useIntl();
|
||||
const { setCurrentTabKey } = useUnitSidebarContext();
|
||||
|
||||
const { selectedPartitionIndex, selectedGroupsLabel } = userPartitionInfo ?? {};
|
||||
const hasGroups = selectedPartitionIndex !== -1 && !Number.isNaN(selectedPartitionIndex) && selectedGroupsLabel;
|
||||
let groupsCount = 0;
|
||||
if (hasGroups) {
|
||||
groupsCount = selectedGroupsLabel.split(',').length;
|
||||
}
|
||||
|
||||
let labelMessages = intl.formatMessage(messages.visibilityAllLearnersTitle);
|
||||
let secondLabelMessages;
|
||||
|
||||
if (visibleToStaffOnly) {
|
||||
labelMessages = intl.formatMessage(messages.visibilityStaffOnlyTitle);
|
||||
} else if (hasGroups) {
|
||||
if (groupsCount === 1) {
|
||||
labelMessages = selectedGroupsLabel;
|
||||
secondLabelMessages = intl.formatMessage(
|
||||
messages.visibilitySingleGroupDetails,
|
||||
{
|
||||
groupName: selectedGroupsLabel,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
labelMessages = intl.formatMessage(messages.visibilityAccessRestrictionsTitle);
|
||||
secondLabelMessages = intl.formatMessage(messages.visibilityMultipleGroupsDetails);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="horizontal" gap={2}>
|
||||
{visibleToStaffOnly ? (
|
||||
<Icon src={Lock} />
|
||||
) : (
|
||||
<Icon src={Groups} />
|
||||
)}
|
||||
<span className="font-weight-bold text-primary-700">
|
||||
{labelMessages}
|
||||
</span>
|
||||
<IconButton
|
||||
src={Edit}
|
||||
alt={intl.formatMessage(messages.visibilityEditButton)}
|
||||
size="inline"
|
||||
onClick={() => setCurrentTabKey('settings')}
|
||||
/>
|
||||
</Stack>
|
||||
{secondLabelMessages}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UnitVisibilityInfo = ({
|
||||
openVisibleModal,
|
||||
visibleToStaffOnly,
|
||||
userPartitionInfo,
|
||||
}: UnitVisibilityInfoProps) => (
|
||||
<>
|
||||
<span className="heading-label">
|
||||
<FormattedMessage {...messages.visibilityVisibleToTitle} />
|
||||
</span>
|
||||
{isUnitPageNewDesignEnabled() ? (
|
||||
<UnitvisibilityInfoContent
|
||||
visibleToStaffOnly={visibleToStaffOnly}
|
||||
userPartitionInfo={userPartitionInfo}
|
||||
/>
|
||||
) : (
|
||||
<LegacyVisibilityInfo
|
||||
visibleToStaffOnly={visibleToStaffOnly}
|
||||
openVisibleModal={openVisibleModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
export default UnitVisibilityInfo;
|
||||
139
src/course-unit/unit-sidebar/unit-info/messages.ts
Normal file
139
src/course-unit/unit-sidebar/unit-info/messages.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
publishInfoDraftSaved: {
|
||||
id: 'course-authoring.course-unit.publish.info.draft.saved',
|
||||
defaultMessage: 'DRAFT SAVED',
|
||||
description: 'Label for the draft date in the publish info section',
|
||||
},
|
||||
publishLastPublished: {
|
||||
id: 'course-authoring.course-unit.publish.info.last.published',
|
||||
defaultMessage: 'LAST PUBLISHED',
|
||||
description: 'Label for the last published date in the publish info section',
|
||||
},
|
||||
modalDiscardUnitChangesTitle: {
|
||||
id: 'course-authoring.course-unit.modal.discard-unit-changes.title',
|
||||
defaultMessage: 'Discard changes',
|
||||
},
|
||||
modalDiscardUnitChangesActionButtonText: {
|
||||
id: 'course-authoring.course-unit.modal.discard-unit-changes.btn.action.text',
|
||||
defaultMessage: 'Discard changes',
|
||||
},
|
||||
modalDiscardUnitChangesCancelButtonText: {
|
||||
id: 'course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
modalDiscardUnitChangesDescription: {
|
||||
id: 'course-authoring.course-unit.modal.discard-unit-changes.description',
|
||||
defaultMessage: 'Are you sure you want to revert to the last published version of the unit? You cannot undo this action.',
|
||||
},
|
||||
modalMakeVisibilityTitle: {
|
||||
id: 'course-authoring.course-unit.modal.make-visibility.title',
|
||||
defaultMessage: 'Make visible to students',
|
||||
},
|
||||
modalMakeVisibilityActionButtonText: {
|
||||
id: 'course-authoring.course-unit.modal.make-visibility.btn.action.text',
|
||||
defaultMessage: 'Make visible to students',
|
||||
},
|
||||
modalMakeVisibilityCancelButtonText: {
|
||||
id: 'course-authoring.course-unit.modal.make-visibility.btn.cancel.text',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
modalMakeVisibilityDescription: {
|
||||
id: 'course-authoring.course-unit.modal.make-visibility.description',
|
||||
defaultMessage: 'If the unit was previously published and released to students, any changes you made to the unit when it was hidden will now be visible to students. Do you want to proceed?',
|
||||
},
|
||||
visibilityVisibleToTitle: {
|
||||
id: 'course-authoring.course-unit.visibility.visible-to.title',
|
||||
defaultMessage: 'VISIBLE TO',
|
||||
description: 'Label for visibility section in the unit sidebar',
|
||||
},
|
||||
visibilityStaffOnlyTitle: {
|
||||
id: 'course-authoring.course-unit.visibility.staff-only.title',
|
||||
defaultMessage: 'Staff only',
|
||||
},
|
||||
visibilityHasExplicitStaffLockText: {
|
||||
id: 'course-authoring.course-unit.visibility.has-explicit-staff-lock.text',
|
||||
defaultMessage: 'with {sectionName}',
|
||||
},
|
||||
visibilityStaffAndLearnersTitle: {
|
||||
id: 'course-authoring.course-unit.visibility.staff-and-learners.title',
|
||||
defaultMessage: 'Staff and learners',
|
||||
},
|
||||
visibilityCheckboxTitle: {
|
||||
id: 'course-authoring.course-unit.visibility.checkbox.title',
|
||||
defaultMessage: 'Hide from learners',
|
||||
},
|
||||
visibilityAccessRestrictionsTitle: {
|
||||
id: 'course-authoring.course-unit.visibility.access-restrictions.title',
|
||||
defaultMessage: 'Access Restrictions Applied',
|
||||
description: 'Label for the access restrictions state',
|
||||
},
|
||||
visibilityAllLearnersTitle: {
|
||||
id: 'course-authoring.course-unit.visibility.all-learners.title',
|
||||
defaultMessage: 'All Learners & Staff',
|
||||
description: 'Label for the all learners state',
|
||||
},
|
||||
visibilitySingleGroupDetails: {
|
||||
id: 'course-authoring.course-unit.visibility.single-group.details',
|
||||
defaultMessage: 'This unit is restricted to {groupName} and Staff',
|
||||
description: 'Details text when the visibility state is access restricted with one group.',
|
||||
},
|
||||
visibilityMultipleGroupsDetails: {
|
||||
id: 'course-authoring.course-unit.visibility.multiple-groups.details',
|
||||
defaultMessage: 'Access to some content in this unit is restricted to specific groups of learners.',
|
||||
description: 'Details text when the visibility state is access restricted with multiple groups',
|
||||
},
|
||||
visibilityEditButton: {
|
||||
id: 'course-authoring.course-unit.visibility.edit.label',
|
||||
defaultMessage: 'Edit Visibility',
|
||||
description: 'Alt label for the edit visibility icon button',
|
||||
},
|
||||
visibilitySaveGroupsButton: {
|
||||
id: 'course-authoring.course-unit.visibility.save-groups.label',
|
||||
defaultMessage: 'Save changes',
|
||||
description: 'Label for the button to save the groups in the settings sidebar.',
|
||||
},
|
||||
sidebarSectionSummary: {
|
||||
id: 'course-authoring.unit-page.sidebar.info.section.summary',
|
||||
defaultMessage: 'Unit Content Summary',
|
||||
description: 'Title for the summary section in the Unit info sidebar',
|
||||
},
|
||||
sidebarSectionTaxonomies: {
|
||||
id: 'course-authoring.unit-page.sidebar.info.section.taxonomies',
|
||||
defaultMessage: 'Taxonomy Alignments',
|
||||
description: 'Title for the taxonomies section in the Unit info sidebar',
|
||||
},
|
||||
sidebarInfoVisibilityStudentLabel: {
|
||||
id: 'course-authoring.unit-page.sidebar.info.settings.student-visibility',
|
||||
defaultMessage: 'Student Visible',
|
||||
description: 'Label the button to make the unit visible to students',
|
||||
},
|
||||
sidebarInfoVisibilityStaffLabel: {
|
||||
id: 'course-authoring.unit-page.sidebar.info.settings.staff-visibility',
|
||||
defaultMessage: 'Staff Only',
|
||||
description: 'Label the button to make the unit visible only to staff',
|
||||
},
|
||||
sidebarInfoVisibilityTitle: {
|
||||
id: 'course-authoring.unit-page.sidebar.info.settings.visibility-title',
|
||||
defaultMessage: 'Visibility',
|
||||
description: 'Title of the Visibility section of the unit sidebar',
|
||||
},
|
||||
sidebarInfoAccessTitle: {
|
||||
id: 'course-authoring.unit-page.sidebar.info.settings.access-title',
|
||||
defaultMessage: 'Access Restrictions',
|
||||
description: 'Title of the access section of the unit sidebar',
|
||||
},
|
||||
sidebarInfoDetailsTab: {
|
||||
id: 'course-authoring.unit-page.sidebar.info.details-tab',
|
||||
defaultMessage: 'Details',
|
||||
description: 'Label for the details tab of the unit info sidebar',
|
||||
},
|
||||
sidebarInfoSettingsTab: {
|
||||
id: 'course-authoring.unit-page.sidebar.info.settings-tab',
|
||||
defaultMessage: 'Settings',
|
||||
description: 'Label for the settings tab of the unit info sidebar',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,7 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
|
||||
interface Props extends Omit<React.ComponentPropsWithoutRef<typeof Alert>, 'title'> {
|
||||
interface Props
|
||||
extends Omit<
|
||||
React.ComponentPropsWithoutRef<typeof Alert>,
|
||||
'title' | 'description'
|
||||
> {
|
||||
title?: string | React.ReactNode;
|
||||
description?: string | React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import messages from './messages';
|
||||
import BasicTab from './BasicTab';
|
||||
import VisibilityTab from './VisibilityTab';
|
||||
import AdvancedTab from './AdvancedTab';
|
||||
import UnitTab from './UnitTab';
|
||||
import { UnitTab } from './UnitTab';
|
||||
|
||||
const ConfigureModal = ({
|
||||
isOpen,
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Form } from '@openedx/paragon';
|
||||
import {
|
||||
FormattedMessage, useIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Field } from 'formik';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { COURSE_BLOCK_NAMES } from '../../constants';
|
||||
import messages from './messages';
|
||||
|
||||
const UnitTab = ({
|
||||
isXBlockComponent,
|
||||
category,
|
||||
values,
|
||||
setFieldValue,
|
||||
showWarning,
|
||||
userPartitionInfo,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
isVisibleToStaffOnly,
|
||||
selectedPartitionIndex,
|
||||
selectedGroups,
|
||||
discussionEnabled,
|
||||
} = values;
|
||||
|
||||
const handleVisibilityChange = (e) => {
|
||||
setFieldValue('isVisibleToStaffOnly', e.target.checked);
|
||||
};
|
||||
|
||||
const handleDiscussionChange = (e) => {
|
||||
setFieldValue('discussionEnabled', e.target.checked);
|
||||
};
|
||||
|
||||
const handleSelect = (e) => {
|
||||
setFieldValue('selectedPartitionIndex', parseInt(e.target.value, 10));
|
||||
setFieldValue('selectedGroups', []);
|
||||
};
|
||||
|
||||
const checkIsDeletedGroup = (group) => {
|
||||
const isGroupSelected = selectedGroups.includes(group.id.toString());
|
||||
|
||||
return group.deleted && isGroupSelected;
|
||||
};
|
||||
|
||||
const getAccessBlockTitle = () => {
|
||||
switch (category) {
|
||||
case COURSE_BLOCK_NAMES.libraryContent.id:
|
||||
return messages.libraryContentAccess;
|
||||
case COURSE_BLOCK_NAMES.splitTest.id:
|
||||
return messages.splitTestAccess;
|
||||
default:
|
||||
return messages.unitAccess;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isXBlockComponent && (
|
||||
<>
|
||||
<h4 className="mt-3"><FormattedMessage {...messages.unitVisibility} /></h4>
|
||||
<hr />
|
||||
<Form.Checkbox checked={isVisibleToStaffOnly} onChange={handleVisibilityChange} data-testid="unit-visibility-checkbox">
|
||||
<FormattedMessage {...messages.hideFromLearners} />
|
||||
</Form.Checkbox>
|
||||
{showWarning && (
|
||||
<Alert className="mt-2" variant="warning">
|
||||
<FormattedMessage {...messages.unitVisibilityWarning} />
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{userPartitionInfo.selectablePartitions.length > 0 && (
|
||||
<Form.Group controlId="groupSelect">
|
||||
<h4 className="mt-3">
|
||||
<FormattedMessage {...getAccessBlockTitle()} />
|
||||
</h4>
|
||||
<hr />
|
||||
<Form.Label as="legend" className="font-weight-bold">
|
||||
<FormattedMessage {...messages.restrictAccessTo} />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
name="groupSelect"
|
||||
value={selectedPartitionIndex}
|
||||
onChange={handleSelect}
|
||||
data-testid="group-type-select"
|
||||
>
|
||||
<option value="-1" key="-1">
|
||||
{userPartitionInfo.selectedPartitionIndex === -1
|
||||
? intl.formatMessage(messages.unitSelectGroupType)
|
||||
: intl.formatMessage(messages.unitAllLearnersAndStaff)}
|
||||
</option>
|
||||
{userPartitionInfo.selectablePartitions.map((partition, index) => (
|
||||
<option
|
||||
key={partition.id}
|
||||
value={index}
|
||||
>
|
||||
{partition.name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
|
||||
{selectedPartitionIndex >= 0 && userPartitionInfo.selectablePartitions.length && (
|
||||
<Form.Group controlId="select-groups-checkboxes">
|
||||
<Form.Label><FormattedMessage {...messages.unitSelectGroup} /></Form.Label>
|
||||
<div
|
||||
role="group"
|
||||
className="d-flex flex-column"
|
||||
data-testid="group-checkboxes"
|
||||
aria-labelledby="select-groups-checkboxes"
|
||||
>
|
||||
{userPartitionInfo.selectablePartitions[selectedPartitionIndex].groups.map((group) => (
|
||||
<Form.Group
|
||||
key={group.id}
|
||||
className="pgn__form-checkbox"
|
||||
>
|
||||
<Field
|
||||
as={Form.Control}
|
||||
className="flex-grow-0 mr-1"
|
||||
controlClassName="pgn__form-checkbox-input mr-1"
|
||||
type="checkbox"
|
||||
value={`${group.id}`}
|
||||
name="selectedGroups"
|
||||
/>
|
||||
<div>
|
||||
<Form.Label
|
||||
className={classNames({ 'text-danger': checkIsDeletedGroup(group) })}
|
||||
isInline
|
||||
>
|
||||
{group.name}
|
||||
</Form.Label>
|
||||
{group.deleted && (
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||
{intl.formatMessage(messages.unitSelectDeletedGroupErrorMessage)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
))}
|
||||
</div>
|
||||
</Form.Group>
|
||||
)}
|
||||
</Form.Group>
|
||||
)}
|
||||
{!isXBlockComponent && (
|
||||
<>
|
||||
<h4 className="mt-4"><FormattedMessage {...messages.discussionEnabledSectionTitle} /></h4>
|
||||
<hr />
|
||||
<Form.Checkbox checked={discussionEnabled} onChange={handleDiscussionChange}>
|
||||
<FormattedMessage {...messages.discussionEnabledCheckbox} />
|
||||
</Form.Checkbox>
|
||||
<p className="x-small font-weight-bold"><FormattedMessage {...messages.discussionEnabledDescription} /></p>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
UnitTab.defaultProps = {
|
||||
isXBlockComponent: false,
|
||||
category: undefined,
|
||||
};
|
||||
|
||||
UnitTab.propTypes = {
|
||||
isXBlockComponent: PropTypes.bool,
|
||||
category: PropTypes.string,
|
||||
values: PropTypes.shape({
|
||||
isVisibleToStaffOnly: PropTypes.bool.isRequired,
|
||||
discussionEnabled: PropTypes.bool,
|
||||
selectedPartitionIndex: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]).isRequired,
|
||||
selectedGroups: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
PropTypes.array,
|
||||
]),
|
||||
}).isRequired,
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
showWarning: PropTypes.bool.isRequired,
|
||||
userPartitionInfo: PropTypes.shape({
|
||||
selectablePartitions: PropTypes.arrayOf(PropTypes.shape({
|
||||
groups: PropTypes.arrayOf(PropTypes.shape({
|
||||
deleted: PropTypes.bool.isRequired,
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
selected: PropTypes.bool.isRequired,
|
||||
}).isRequired).isRequired,
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
scheme: PropTypes.string.isRequired,
|
||||
}).isRequired).isRequired,
|
||||
selectedGroupsLabel: PropTypes.string,
|
||||
selectedPartitionIndex: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default UnitTab;
|
||||
228
src/generic/configure-modal/UnitTab.tsx
Normal file
228
src/generic/configure-modal/UnitTab.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { Alert, Form } from '@openedx/paragon';
|
||||
import {
|
||||
FormattedMessage, useIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Field } from 'formik';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { COURSE_BLOCK_NAMES } from '../../constants';
|
||||
import messages from './messages';
|
||||
|
||||
export type UserpartitionInfo = {
|
||||
selectablePartitions: {
|
||||
groups: {
|
||||
deleted: boolean,
|
||||
id: number,
|
||||
name: string,
|
||||
selected: boolean,
|
||||
}[],
|
||||
id: number,
|
||||
name: string,
|
||||
scheme: string,
|
||||
}[],
|
||||
selectedGroupsLabel?: string,
|
||||
selectedPartitionIndex: number,
|
||||
};
|
||||
|
||||
export interface UnitTabProps {
|
||||
isXBlockComponent: boolean,
|
||||
category?: string,
|
||||
values: {
|
||||
isVisibleToStaffOnly: boolean,
|
||||
discussionEnabled: boolean,
|
||||
selectedPartitionIndex: number,
|
||||
selectedGroups: string[],
|
||||
},
|
||||
setFieldValue: (key: string, value: any) => void,
|
||||
showWarning: boolean,
|
||||
userPartitionInfo: UserpartitionInfo,
|
||||
}
|
||||
|
||||
export const DiscussionEditComponent = ({
|
||||
discussionEnabled,
|
||||
handleDiscussionChange,
|
||||
}: {
|
||||
discussionEnabled: boolean,
|
||||
handleDiscussionChange: (e: any) => void,
|
||||
}) => (
|
||||
<>
|
||||
<Form.Checkbox checked={discussionEnabled} onChange={handleDiscussionChange}>
|
||||
<FormattedMessage {...messages.discussionEnabledCheckbox} />
|
||||
</Form.Checkbox>
|
||||
<p className="x-small font-weight-bold"><FormattedMessage {...messages.discussionEnabledDescription} /></p>
|
||||
</>
|
||||
);
|
||||
|
||||
export interface AccessEditComponentProps {
|
||||
selectedPartitionIndex: number,
|
||||
setFieldValue: (key: string, value: any) => void,
|
||||
userPartitionInfo: UserpartitionInfo,
|
||||
selectedGroups: string[],
|
||||
}
|
||||
|
||||
export const AccessEditComponent = ({
|
||||
selectedPartitionIndex,
|
||||
setFieldValue,
|
||||
userPartitionInfo,
|
||||
selectedGroups,
|
||||
}: AccessEditComponentProps) => {
|
||||
const intl = useIntl();
|
||||
const checkIsDeletedGroup = (group) => {
|
||||
const isGroupSelected = selectedGroups.includes(group.id.toString());
|
||||
|
||||
return group.deleted && isGroupSelected;
|
||||
};
|
||||
|
||||
const handleSelect = (e) => {
|
||||
setFieldValue('selectedPartitionIndex', parseInt(e.target.value, 10));
|
||||
setFieldValue('selectedGroups', selectedGroups);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Label className="font-weight-bold">
|
||||
<FormattedMessage {...messages.restrictAccessTo} />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
name="groupSelect"
|
||||
value={selectedPartitionIndex}
|
||||
onChange={handleSelect}
|
||||
data-testid="group-type-select"
|
||||
>
|
||||
<option value="-1" key="-1">
|
||||
{userPartitionInfo.selectedPartitionIndex === -1
|
||||
? intl.formatMessage(messages.unitSelectGroupType)
|
||||
: intl.formatMessage(messages.unitAllLearnersAndStaff)}
|
||||
</option>
|
||||
{userPartitionInfo.selectablePartitions.map((partition, index) => (
|
||||
<option
|
||||
key={partition.id}
|
||||
value={index}
|
||||
>
|
||||
{partition.name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
|
||||
{selectedPartitionIndex >= 0 && userPartitionInfo.selectablePartitions.length && (
|
||||
<Form.Group controlId="select-groups-checkboxes">
|
||||
<Form.Label><FormattedMessage {...messages.unitSelectGroup} /></Form.Label>
|
||||
<div
|
||||
role="group"
|
||||
className="d-flex flex-column"
|
||||
data-testid="group-checkboxes"
|
||||
aria-labelledby="select-groups-checkboxes"
|
||||
>
|
||||
{userPartitionInfo.selectablePartitions[selectedPartitionIndex].groups.map((group) => (
|
||||
<Form.Group
|
||||
key={group.id}
|
||||
className="pgn__form-checkbox"
|
||||
>
|
||||
<Field
|
||||
as={Form.Control}
|
||||
className="flex-grow-0 mr-1"
|
||||
controlClassName="pgn__form-checkbox-input mr-1"
|
||||
type="checkbox"
|
||||
value={`${group.id}`}
|
||||
name="selectedGroups"
|
||||
/>
|
||||
<div>
|
||||
<Form.Label
|
||||
className={classNames({ 'text-danger': checkIsDeletedGroup(group) })}
|
||||
isInline
|
||||
>
|
||||
{group.name}
|
||||
</Form.Label>
|
||||
{/* istanbul ignore next */ group.deleted && (
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||
<FormattedMessage {...messages.unitSelectDeletedGroupErrorMessage} />
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
))}
|
||||
</div>
|
||||
</Form.Group>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const UnitTab = ({
|
||||
isXBlockComponent,
|
||||
category,
|
||||
values,
|
||||
setFieldValue,
|
||||
showWarning,
|
||||
userPartitionInfo,
|
||||
}: UnitTabProps) => {
|
||||
const {
|
||||
isVisibleToStaffOnly,
|
||||
selectedPartitionIndex,
|
||||
selectedGroups,
|
||||
discussionEnabled,
|
||||
} = values;
|
||||
|
||||
const handleVisibilityChange = (e) => {
|
||||
setFieldValue('isVisibleToStaffOnly', e.target.checked);
|
||||
};
|
||||
|
||||
const handleDiscussionChange = (e) => {
|
||||
setFieldValue('discussionEnabled', e.target.checked);
|
||||
};
|
||||
|
||||
const getAccessBlockTitle = () => {
|
||||
switch (category) {
|
||||
case COURSE_BLOCK_NAMES.libraryContent.id:
|
||||
return messages.libraryContentAccess;
|
||||
case COURSE_BLOCK_NAMES.splitTest.id:
|
||||
return messages.splitTestAccess;
|
||||
default:
|
||||
return messages.unitAccess;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isXBlockComponent && (
|
||||
<>
|
||||
<h4 className="mt-3"><FormattedMessage {...messages.unitVisibility} /></h4>
|
||||
<hr />
|
||||
<Form.Checkbox checked={isVisibleToStaffOnly} onChange={handleVisibilityChange} data-testid="unit-visibility-checkbox">
|
||||
<FormattedMessage {...messages.hideFromLearners} />
|
||||
</Form.Checkbox>
|
||||
{/* istanbul ignore next */ showWarning && (
|
||||
<Alert className="mt-2" variant="warning">
|
||||
<FormattedMessage {...messages.unitVisibilityWarning} />
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{userPartitionInfo.selectablePartitions.length > 0 && (
|
||||
<Form.Group controlId="groupSelect">
|
||||
<h4 className="mt-3">
|
||||
<FormattedMessage {...getAccessBlockTitle()} />
|
||||
</h4>
|
||||
<hr />
|
||||
<AccessEditComponent
|
||||
selectedPartitionIndex={selectedPartitionIndex}
|
||||
setFieldValue={setFieldValue}
|
||||
userPartitionInfo={userPartitionInfo}
|
||||
selectedGroups={selectedGroups}
|
||||
/>
|
||||
</Form.Group>
|
||||
)}
|
||||
{!isXBlockComponent && (
|
||||
<>
|
||||
<h4 className="mt-4"><FormattedMessage {...messages.discussionEnabledSectionTitle} /></h4>
|
||||
<hr />
|
||||
<DiscussionEditComponent
|
||||
discussionEnabled={discussionEnabled}
|
||||
handleDiscussionChange={handleDiscussionChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -91,7 +91,7 @@ export function Sidebar<T extends SidebarPages>({
|
||||
return (
|
||||
<Stack direction="horizontal" className="sidebar align-items-baseline ml-3" gap={2}>
|
||||
{isOpen && !!currentPageKey && (
|
||||
<div className="sidebar-content p-2 bg-white border-right">
|
||||
<div className="sidebar-content p-3 bg-white border-right">
|
||||
<Dropdown data-testid="sidebar-dropdown">
|
||||
<Dropdown.Toggle
|
||||
id="dropdown-toggle-with-iconbutton"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.sidebar-content {
|
||||
flex: 0 1 auto;
|
||||
max-width: 700px;
|
||||
min-width: 650px;
|
||||
min-width: 400px;
|
||||
overflow-y: auto;
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework/dist';
|
||||
import { Stack } from '@openedx/paragon';
|
||||
import TagsSidebarControls from '../../content-tags-drawer/tags-sidebar-controls';
|
||||
import Sidebar from '../../course-unit/sidebar';
|
||||
import LocationInfo from '../../course-unit/sidebar/LocationInfo';
|
||||
import PublishControls from '../../course-unit/sidebar/PublishControls';
|
||||
import SplitTestSidebarInfo from '../../course-unit/sidebar/SplitTestSidebarInfo';
|
||||
import { UnitSidebar } from '@src/course-unit/unit-sidebar/UnitSidebar';
|
||||
|
||||
export const CourseAuthoringUnitSidebarSlot = (
|
||||
{
|
||||
@@ -24,34 +18,15 @@ export const CourseAuthoringUnitSidebarSlot = (
|
||||
blockId, courseId, unitTitle, xBlocks, readOnly, isUnitVerticalType, isSplitTestType,
|
||||
}}
|
||||
>
|
||||
<Stack gap={3}>
|
||||
{isUnitVerticalType && (
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.authoring.course_unit_sidebar.v1"
|
||||
idAliases={['course_authoring_unit_sidebar_slot']}
|
||||
pluginProps={{
|
||||
blockId, courseId, unitTitle, xBlocks, readOnly,
|
||||
}}
|
||||
>
|
||||
<Sidebar data-testid="course-unit-sidebar">
|
||||
<PublishControls blockId={blockId} />
|
||||
</Sidebar>
|
||||
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
|
||||
<Sidebar className="tags-sidebar">
|
||||
<TagsSidebarControls readOnly={readOnly} />
|
||||
</Sidebar>
|
||||
)}
|
||||
<Sidebar data-testid="course-unit-location-sidebar">
|
||||
<LocationInfo />
|
||||
</Sidebar>
|
||||
</PluginSlot>
|
||||
)}
|
||||
{isSplitTestType && (
|
||||
<Sidebar data-testid="course-split-test-sidebar">
|
||||
<SplitTestSidebarInfo />
|
||||
</Sidebar>
|
||||
)}
|
||||
</Stack>
|
||||
<UnitSidebar
|
||||
legacySidebarProps={{
|
||||
unitTitle,
|
||||
xBlocks,
|
||||
readOnly,
|
||||
isUnitVerticalType,
|
||||
isSplitTestType,
|
||||
}}
|
||||
/>
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user