From 5157cbcfb2d9d0d6b8c6a8086396248edb087833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Mon, 26 Jan 2026 16:18:32 -0500 Subject: [PATCH] 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. --- .env.development | 2 +- .env.test | 2 +- src/course-unit/CourseUnit.scss | 3 +- src/course-unit/CourseUnit.test.jsx | 788 +++++++++++++----- src/course-unit/CourseUnit.tsx | 63 +- src/course-unit/constants.ts | 5 +- src/course-unit/data/api.ts | 4 +- src/course-unit/data/thunk.js | 2 +- src/course-unit/hooks.jsx | 38 +- src/course-unit/hooks.test.jsx | 54 +- .../LegacySidebar.scss} | 7 - .../LocationInfo.jsx | 0 .../SidebarSection.tsx} | 6 +- .../SplitTestSidebarInfo.tsx | 0 .../components/ReleaseInfoComponent.jsx | 0 .../components/SidebarBody.jsx | 0 .../components/SidebarHeader.jsx | 2 +- .../components/index.js | 0 .../sidebar-footer/ActionButtons.test.jsx | 0 .../sidebar-footer/ActionButtons.tsx | 12 +- .../components/sidebar-footer/index.tsx | 22 +- .../{sidebar => legacy-sidebar}/hooks.jsx | 11 + src/course-unit/legacy-sidebar/index.tsx | 78 ++ .../{sidebar => legacy-sidebar}/messages.ts | 56 -- .../{sidebar => legacy-sidebar}/utils.js | 29 +- src/course-unit/sidebar/PublishControls.tsx | 97 --- .../sidebar-footer/UnitVisibilityInfo.tsx | 69 -- src/course-unit/unit-sidebar/UnitSidebar.tsx | 36 + .../unit-sidebar/UnitSidebarContext.tsx | 67 ++ src/course-unit/unit-sidebar/constants.ts | 20 + src/course-unit/unit-sidebar/messages.ts | 11 + .../unit-info/PublishControls.scss | 19 + .../unit-info/PublishControls.tsx | 174 ++++ .../unit-info/UnitInfoSidebar.tsx | 238 ++++++ .../unit-info/UnitVisibilityInfo.tsx | 165 ++++ .../unit-sidebar/unit-info/messages.ts | 139 +++ src/generic/alert-message/index.tsx | 6 +- .../configure-modal/ConfigureModal.jsx | 2 +- src/generic/configure-modal/UnitTab.jsx | 201 ----- src/generic/configure-modal/UnitTab.tsx | 228 +++++ src/generic/sidebar/Sidebar.tsx | 2 +- src/generic/sidebar/index.scss | 2 +- .../CourseAuthoringUnitSidebarSlot/index.tsx | 45 +- 43 files changed, 1844 insertions(+), 861 deletions(-) rename src/course-unit/{sidebar/Sidebar.scss => legacy-sidebar/LegacySidebar.scss} (92%) rename src/course-unit/{sidebar => legacy-sidebar}/LocationInfo.jsx (100%) rename src/course-unit/{sidebar/index.tsx => legacy-sidebar/SidebarSection.tsx} (62%) rename src/course-unit/{sidebar => legacy-sidebar}/SplitTestSidebarInfo.tsx (100%) rename src/course-unit/{sidebar => legacy-sidebar}/components/ReleaseInfoComponent.jsx (100%) rename src/course-unit/{sidebar => legacy-sidebar}/components/SidebarBody.jsx (100%) rename src/course-unit/{sidebar => legacy-sidebar}/components/SidebarHeader.jsx (98%) rename src/course-unit/{sidebar => legacy-sidebar}/components/index.js (100%) rename src/course-unit/{sidebar => legacy-sidebar}/components/sidebar-footer/ActionButtons.test.jsx (100%) rename src/course-unit/{sidebar => legacy-sidebar}/components/sidebar-footer/ActionButtons.tsx (82%) rename src/course-unit/{sidebar => legacy-sidebar}/components/sidebar-footer/index.tsx (66%) rename src/course-unit/{sidebar => legacy-sidebar}/hooks.jsx (80%) create mode 100644 src/course-unit/legacy-sidebar/index.tsx rename src/course-unit/{sidebar => legacy-sidebar}/messages.ts (73%) rename src/course-unit/{sidebar => legacy-sidebar}/utils.js (73%) delete mode 100644 src/course-unit/sidebar/PublishControls.tsx delete mode 100644 src/course-unit/sidebar/components/sidebar-footer/UnitVisibilityInfo.tsx create mode 100644 src/course-unit/unit-sidebar/UnitSidebar.tsx create mode 100644 src/course-unit/unit-sidebar/UnitSidebarContext.tsx create mode 100644 src/course-unit/unit-sidebar/constants.ts create mode 100644 src/course-unit/unit-sidebar/messages.ts create mode 100644 src/course-unit/unit-sidebar/unit-info/PublishControls.scss create mode 100644 src/course-unit/unit-sidebar/unit-info/PublishControls.tsx create mode 100644 src/course-unit/unit-sidebar/unit-info/UnitInfoSidebar.tsx create mode 100644 src/course-unit/unit-sidebar/unit-info/UnitVisibilityInfo.tsx create mode 100644 src/course-unit/unit-sidebar/unit-info/messages.ts delete mode 100644 src/generic/configure-modal/UnitTab.jsx create mode 100644 src/generic/configure-modal/UnitTab.tsx diff --git a/.env.development b/.env.development index e1c7709be..8771be283 100644 --- a/.env.development +++ b/.env.development @@ -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 diff --git a/.env.test b/.env.test index 9757e8faf..0421e8099 100644 --- a/.env.test +++ b/.env.test @@ -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 diff --git a/src/course-unit/CourseUnit.scss b/src/course-unit/CourseUnit.scss index b2fbc8502..a73e8e192 100644 --- a/src/course-unit/CourseUnit.scss +++ b/src/course-unit/CourseUnit.scss @@ -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"; diff --git a/src/course-unit/CourseUnit.test.jsx b/src/course-unit/CourseUnit.test.jsx index b83238178..fcfc0b33b 100644 --- a/src/course-unit/CourseUnit.test.jsx +++ b/src/course-unit/CourseUnit.test.jsx @@ -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('', () => { 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(); - 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('', () => { it('displays an error alert when a studioAjaxError message is received', async () => { render(); - 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(); - 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('', () => { it('closes the legacy edit modal when closeXBlockEditorModal message is received', async () => { render(); - 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(); - 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('', () => { }, }); - 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(); - 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('', () => { }, }); - 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('', () => { 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('', () => { .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('', () => { }); 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('', () => { 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('', () => { ); // 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('', () => { render(); 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('', () => { 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('', () => { render(); 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('', () => { 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('', () => { 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('', () => { render(); 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('', () => { 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('', () => { 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('', () => { render(); 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { ), store.dispatch); expect(within(courseUnitSidebar) - .getByText(sidebarMessages.sidebarTitlePublishedNotYetReleased.defaultMessage)).toBeInTheDocument(); + .getByText(legacySidebarMessages.sidebarTitlePublishedNotYetReleased.defaultMessage)).toBeInTheDocument(); expect(discardChangesBtn).not.toBeInTheDocument(); }); @@ -1300,7 +1279,7 @@ describe('', () => { 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('', () => { 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('', () => { 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('', () => { }); 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('', () => { 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('', () => { 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('', () => { 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('', () => { // 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('', () => { }); expect( - screen.queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }), + screen.queryByRole('link', { name: legacySidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }), ).toHaveAttribute('href', helpLinkUrl); }); }); @@ -2338,7 +2317,7 @@ describe('', () => { 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('', () => { 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(); + + 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('', () => { expect(await screen.findByText('Live')).toBeInTheDocument(); }); + it('should change the visibility of the unit in the settings sidebar', async () => { + const user = userEvent.setup(); + render(); + + 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('', () => { }); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); render(); - 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(); + + 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(); + + 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(); + + 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(); + + 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('', () => { 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('', () => { 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, + }, + ], + }], }, }, }); diff --git a/src/course-unit/CourseUnit.tsx b/src/course-unit/CourseUnit.tsx index 938b43c94..20771737a 100644 --- a/src/course-unit/CourseUnit.tsx +++ b/src/course-unit/CourseUnit.tsx @@ -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 ( - <> - + +
{movedXBlockParams.isSuccess ? ( @@ -321,8 +334,8 @@ const CourseUnit = () => { showPasteUnit={showPasteUnit} /> )} - - +
+
{currentlyVisibleToStudents && ( { courseId={courseId} /> - - - {blockId && ( - - )} - - +
+ {!isUnitLegacyLibraryType && ( + + )} +
@@ -400,7 +411,7 @@ const CourseUnit = () => { errorMessage={errorMessage} />
- +
); }; diff --git a/src/course-unit/constants.ts b/src/course-unit/constants.ts index 04f9fa889..96d7d60f6 100644 --- a/src/course-unit/constants.ts +++ b/src/course-unit/constants.ts @@ -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 = { diff --git a/src/course-unit/data/api.ts b/src/course-unit/data/api.ts index c8c3e6f98..d4474ed5f 100644 --- a/src/course-unit/data/api.ts +++ b/src/course-unit/data/api.ts @@ -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 | null, ): Promise { 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 }), }, } : {}), }; diff --git a/src/course-unit/data/thunk.js b/src/course-unit/data/thunk.js index cd75aec92..b160a1fb0 100644 --- a/src/course-unit/data/thunk.js +++ b/src/course-unit/data/thunk.js @@ -117,8 +117,8 @@ export function editCourseUnitVisibilityAndData( itemId, type, isVisible, - groupAccess, isDiscussionEnabled, + groupAccess, ).then(async (result) => { if (result) { if (callback) { diff --git a/src/course-unit/hooks.jsx b/src/course-unit/hooks.jsx index 14d7403e4..0a58e2259 100644 --- a/src/course-unit/hooks.jsx +++ b/src/course-unit/hooks.jsx @@ -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 diff --git a/src/course-unit/hooks.test.jsx b/src/course-unit/hooks.test.jsx index cec7ab5e5..c1c4efa93 100644 --- a/src/course-unit/hooks.test.jsx +++ b/src/course-unit/hooks.test.jsx @@ -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; diff --git a/src/course-unit/sidebar/Sidebar.scss b/src/course-unit/legacy-sidebar/LegacySidebar.scss similarity index 92% rename from src/course-unit/sidebar/Sidebar.scss rename to src/course-unit/legacy-sidebar/LegacySidebar.scss index 58416091b..18892a1df 100644 --- a/src/course-unit/sidebar/Sidebar.scss +++ b/src/course-unit/legacy-sidebar/LegacySidebar.scss @@ -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; } diff --git a/src/course-unit/sidebar/LocationInfo.jsx b/src/course-unit/legacy-sidebar/LocationInfo.jsx similarity index 100% rename from src/course-unit/sidebar/LocationInfo.jsx rename to src/course-unit/legacy-sidebar/LocationInfo.jsx diff --git a/src/course-unit/sidebar/index.tsx b/src/course-unit/legacy-sidebar/SidebarSection.tsx similarity index 62% rename from src/course-unit/sidebar/index.tsx rename to src/course-unit/legacy-sidebar/SidebarSection.tsx index 98f3f28c6..2feccdcc8 100644 --- a/src/course-unit/sidebar/index.tsx +++ b/src/course-unit/legacy-sidebar/SidebarSection.tsx @@ -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) => ( ); -interface SidebarProps { +interface SidebarSectionProps { className?: string | null; children?: React.ReactNode | null; } -export default Sidebar; +export default SidebarSection; diff --git a/src/course-unit/sidebar/SplitTestSidebarInfo.tsx b/src/course-unit/legacy-sidebar/SplitTestSidebarInfo.tsx similarity index 100% rename from src/course-unit/sidebar/SplitTestSidebarInfo.tsx rename to src/course-unit/legacy-sidebar/SplitTestSidebarInfo.tsx diff --git a/src/course-unit/sidebar/components/ReleaseInfoComponent.jsx b/src/course-unit/legacy-sidebar/components/ReleaseInfoComponent.jsx similarity index 100% rename from src/course-unit/sidebar/components/ReleaseInfoComponent.jsx rename to src/course-unit/legacy-sidebar/components/ReleaseInfoComponent.jsx diff --git a/src/course-unit/sidebar/components/SidebarBody.jsx b/src/course-unit/legacy-sidebar/components/SidebarBody.jsx similarity index 100% rename from src/course-unit/sidebar/components/SidebarBody.jsx rename to src/course-unit/legacy-sidebar/components/SidebarBody.jsx diff --git a/src/course-unit/sidebar/components/SidebarHeader.jsx b/src/course-unit/legacy-sidebar/components/SidebarHeader.jsx similarity index 98% rename from src/course-unit/sidebar/components/SidebarHeader.jsx rename to src/course-unit/legacy-sidebar/components/SidebarHeader.jsx index b6b6feda0..1f7a25d20 100644 --- a/src/course-unit/sidebar/components/SidebarHeader.jsx +++ b/src/course-unit/legacy-sidebar/components/SidebarHeader.jsx @@ -13,7 +13,7 @@ const SidebarHeader = ({ title, visibilityState, displayUnitLocation }) => { const { iconSrc, colorVariant } = getIconVariant(visibilityState, published, hasChanges); return ( - + {!displayUnitLocation && ( void, handlePublishing: () => void, + hideCopyButton?: boolean, } const ActionButtons = ({ openDiscardModal, handlePublishing, + hideCopyButton = false, }: ActionButtonsProps) => { const intl = useIntl(); const { @@ -32,7 +34,7 @@ const ActionButtons = ({ )} - {enableCopyPasteUnits && canEdit && ( + {enableCopyPasteUnits && canEdit && !hideCopyButton && ( <> + + + + + + {({ + values, setFieldValue, dirty, + }) => ( +
+ + {dirty && ( + + )} + + )} +
+
+ + handleUpdate(visibleToStaffOnly, null, e.target.checked)} + /> + + + ); +}; + +/** + * 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 ( +
+ + + +
+ +
+
+ +
+ +
+
+
+
+ ); +}; diff --git a/src/course-unit/unit-sidebar/unit-info/UnitVisibilityInfo.tsx b/src/course-unit/unit-sidebar/unit-info/UnitVisibilityInfo.tsx new file mode 100644 index 000000000..9f6b69b3b --- /dev/null +++ b/src/course-unit/unit-sidebar/unit-info/UnitVisibilityInfo.tsx @@ -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[], + selectedGroupsLabel: string, + selectedPartitionIndex: number, + }, +} + +interface UnitvisibilityInfoContentProps { + visibleToStaffOnly: boolean, + userPartitionInfo?: { + selectablePartitions: Record[], + 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 ? ( + <> +
+ +
+ {/* istanbul ignore next */ !hasExplicitStaffLock && ( + + + + )} + + ) : ( +
+ +
+ )} + + + + + ); +}; + +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 ( + <> + + {visibleToStaffOnly ? ( + + ) : ( + + )} + + {labelMessages} + + setCurrentTabKey('settings')} + /> + + {secondLabelMessages} + + ); +}; + +const UnitVisibilityInfo = ({ + openVisibleModal, + visibleToStaffOnly, + userPartitionInfo, +}: UnitVisibilityInfoProps) => ( + <> + + + + {isUnitPageNewDesignEnabled() ? ( + + ) : ( + + )} + +); + +export default UnitVisibilityInfo; diff --git a/src/course-unit/unit-sidebar/unit-info/messages.ts b/src/course-unit/unit-sidebar/unit-info/messages.ts new file mode 100644 index 000000000..9201916f8 --- /dev/null +++ b/src/course-unit/unit-sidebar/unit-info/messages.ts @@ -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; diff --git a/src/generic/alert-message/index.tsx b/src/generic/alert-message/index.tsx index 16f84e93a..182b23efd 100644 --- a/src/generic/alert-message/index.tsx +++ b/src/generic/alert-message/index.tsx @@ -1,7 +1,11 @@ import React from 'react'; import { Alert } from '@openedx/paragon'; -interface Props extends Omit, 'title'> { +interface Props + extends Omit< + React.ComponentPropsWithoutRef, + 'title' | 'description' + > { title?: string | React.ReactNode; description?: string | React.ReactNode; } diff --git a/src/generic/configure-modal/ConfigureModal.jsx b/src/generic/configure-modal/ConfigureModal.jsx index 9d5658ed5..e34dfbcc1 100644 --- a/src/generic/configure-modal/ConfigureModal.jsx +++ b/src/generic/configure-modal/ConfigureModal.jsx @@ -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, diff --git a/src/generic/configure-modal/UnitTab.jsx b/src/generic/configure-modal/UnitTab.jsx deleted file mode 100644 index 345b5093e..000000000 --- a/src/generic/configure-modal/UnitTab.jsx +++ /dev/null @@ -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 && ( - <> -

-
- - - - {showWarning && ( - - - - )} - - )} - {userPartitionInfo.selectablePartitions.length > 0 && ( - -

- -

-
- - - - - - {userPartitionInfo.selectablePartitions.map((partition, index) => ( - - ))} - - - {selectedPartitionIndex >= 0 && userPartitionInfo.selectablePartitions.length && ( - - -
- {userPartitionInfo.selectablePartitions[selectedPartitionIndex].groups.map((group) => ( - - -
- - {group.name} - - {group.deleted && ( - - {intl.formatMessage(messages.unitSelectDeletedGroupErrorMessage)} - - )} -
-
- ))} -
-
- )} -
- )} - {!isXBlockComponent && ( - <> -

-
- - - -

- - )} - - ); -}; - -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; diff --git a/src/generic/configure-modal/UnitTab.tsx b/src/generic/configure-modal/UnitTab.tsx new file mode 100644 index 000000000..ea189ed2d --- /dev/null +++ b/src/generic/configure-modal/UnitTab.tsx @@ -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, +}) => ( + <> + + + +

+ +); + +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 ( + <> + + + + + + {userPartitionInfo.selectablePartitions.map((partition, index) => ( + + ))} + + + {selectedPartitionIndex >= 0 && userPartitionInfo.selectablePartitions.length && ( + + +
+ {userPartitionInfo.selectablePartitions[selectedPartitionIndex].groups.map((group) => ( + + +
+ + {group.name} + + {/* istanbul ignore next */ group.deleted && ( + + + + )} +
+
+ ))} +
+
+ )} + + ); +}; + +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 && ( + <> +

+
+ + + + {/* istanbul ignore next */ showWarning && ( + + + + )} + + )} + {userPartitionInfo.selectablePartitions.length > 0 && ( + +

+ +

+
+ +
+ )} + {!isXBlockComponent && ( + <> +

+
+ + + )} + + ); +}; diff --git a/src/generic/sidebar/Sidebar.tsx b/src/generic/sidebar/Sidebar.tsx index 9ca5c2999..2e08b5d9f 100644 --- a/src/generic/sidebar/Sidebar.tsx +++ b/src/generic/sidebar/Sidebar.tsx @@ -91,7 +91,7 @@ export function Sidebar({ return ( {isOpen && !!currentPageKey && ( -
+
- - {isUnitVerticalType && ( - - - - - {getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && ( - - - - )} - - - - - )} - {isSplitTestType && ( - - - - )} - + );