diff --git a/package-lock.json b/package-lock.json index 2df108aad5..2f013f58d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21855,6 +21855,7 @@ } }, "plugins/course-apps/calculator": { + "name": "@openedx-plugins/course-app-calculator", "version": "0.1.0", "peerDependencies": { "@edx/frontend-app-course-authoring": "*", @@ -21870,6 +21871,7 @@ } }, "plugins/course-apps/edxnotes": { + "name": "@openedx-plugins/course-app-edxnotes", "version": "0.1.0", "peerDependencies": { "@edx/frontend-app-course-authoring": "*", @@ -21885,6 +21887,7 @@ } }, "plugins/course-apps/learning_assistant": { + "name": "@openedx-plugins/course-app-learning_assistant", "version": "0.1.0", "peerDependencies": { "@edx/frontend-app-course-authoring": "*", @@ -21901,6 +21904,7 @@ } }, "plugins/course-apps/live": { + "name": "@openedx-plugins/course-app-live", "version": "0.1.0", "peerDependencies": { "@edx/frontend-app-course-authoring": "*", @@ -21921,6 +21925,7 @@ } }, "plugins/course-apps/ora_settings": { + "name": "@openedx-plugins/course-app-ora_settings", "version": "0.1.0", "peerDependencies": { "@edx/frontend-app-course-authoring": "*", @@ -21937,6 +21942,7 @@ } }, "plugins/course-apps/proctoring": { + "name": "@openedx-plugins/course-app-proctoring", "version": "0.1.0", "peerDependencies": { "@edx/frontend-app-course-authoring": "*", @@ -21955,6 +21961,7 @@ } }, "plugins/course-apps/progress": { + "name": "@openedx-plugins/course-app-progress", "version": "0.1.0", "peerDependencies": { "@edx/frontend-app-course-authoring": "*", @@ -21971,6 +21978,7 @@ } }, "plugins/course-apps/teams": { + "name": "@openedx-plugins/course-app-teams", "version": "0.1.0", "peerDependencies": { "@edx/frontend-app-course-authoring": "*", @@ -21989,6 +21997,7 @@ } }, "plugins/course-apps/wiki": { + "name": "@openedx-plugins/course-app-wiki", "version": "0.1.0", "peerDependencies": { "@edx/frontend-app-course-authoring": "*", @@ -22005,6 +22014,7 @@ } }, "plugins/course-apps/xpert_unit_summary": { + "name": "@openedx-plugins/course-app-xpert_unit_summary", "version": "0.1.0", "peerDependencies": { "@edx/frontend-app-course-authoring": "*", diff --git a/src/constants.js b/src/constants.js index eb1b17b372..2913884a94 100644 --- a/src/constants.js +++ b/src/constants.js @@ -26,6 +26,10 @@ export const NOTIFICATION_MESSAGES = { deleting: 'Deleting', copying: 'Copying', pasting: 'Pasting', + discardChanges: 'Discarding changes', + publishing: 'Publishing', + hidingFromStudents: 'Hiding from students', + makingVisibleToStudents: 'Making visible to students', empty: '', }; diff --git a/src/course-outline/CourseOutline.jsx b/src/course-outline/CourseOutline.jsx index 986969ee56..5710fd01cd 100644 --- a/src/course-outline/CourseOutline.jsx +++ b/src/course-outline/CourseOutline.jsx @@ -24,8 +24,11 @@ import { RequestStatus } from '../data/constants'; import SubHeader from '../generic/sub-header/SubHeader'; import ProcessingNotification from '../generic/processing-notification'; import InternetConnectionAlert from '../generic/internet-connection-alert'; +import DeleteModal from '../generic/delete-modal/DeleteModal'; import AlertMessage from '../generic/alert-message'; import getPageHeadTitle from '../generic/utils'; +import { getCurrentItem } from './data/selectors'; +import { COURSE_BLOCK_NAMES } from './constants'; import HeaderNavigations from './header-navigations/HeaderNavigations'; import OutlineSideBar from './outline-sidebar/OutlineSidebar'; import StatusBar from './status-bar/StatusBar'; @@ -37,7 +40,6 @@ import HighlightsModal from './highlights-modal/HighlightsModal'; import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder'; import PublishModal from './publish-modal/PublishModal'; import ConfigureModal from './configure-modal/ConfigureModal'; -import DeleteModal from './delete-modal/DeleteModal'; import PageAlerts from './page-alerts/PageAlerts'; import { useCourseOutline } from './hooks'; import messages from './messages'; @@ -115,6 +117,9 @@ const CourseOutline = ({ courseId }) => { title: processingNotificationTitle, } = useSelector(getProcessingNotification); + const { category } = useSelector(getCurrentItem); + const deleteCategory = COURSE_BLOCK_NAMES[category]?.name.toLowerCase(); + const finalizeSectionOrder = () => (newSections) => { initialSections = [...sectionsList]; handleSectionDragAndDrop(newSections.map(section => section.id), () => { @@ -459,6 +464,7 @@ const CourseOutline = ({ courseId }) => { onConfigureSubmit={handleConfigureItemSubmit} /> { savingStatus, isTitleEditFormOpen, isErrorAlert, + currentlyVisibleToStudents, isInternetConnectionAlertFailed, + unitXBlockActions, handleTitleEditSubmit, headerNavigationsActions, handleTitleEdit, handleInternetConnectionFailed, handleCreateNewCourseXBlock, + courseVerticalChildren, } = useCourseUnit({ courseId, blockId }); document.title = getPageHeadTitle('', unitTitle); @@ -83,19 +90,43 @@ const CourseUnit = ({ courseId }) => { handleCreateNewCourseXBlock={handleCreateNewCourseXBlock} /> + {currentlyVisibleToStudents && ( + + )} + + {courseVerticalChildren.children.map(({ name, blockId: id, shouldScroll }) => ( + + ))} + - + + + + + + diff --git a/src/course-unit/CourseUnit.scss b/src/course-unit/CourseUnit.scss index d3264d89f2..270691ecae 100644 --- a/src/course-unit/CourseUnit.scss +++ b/src/course-unit/CourseUnit.scss @@ -1,3 +1,5 @@ @import "./breadcrumbs/Breadcrumbs"; @import "./course-sequence/CourseSequence"; @import "./add-component/AddComponent"; +@import "./course-xblock/CourseXBlock"; +@import "./sidebar/Sidebar"; diff --git a/src/course-unit/CourseUnit.test.jsx b/src/course-unit/CourseUnit.test.jsx index 15b94b4382..233bb63cc1 100644 --- a/src/course-unit/CourseUnit.test.jsx +++ b/src/course-unit/CourseUnit.test.jsx @@ -1,6 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; import { - act, render, waitFor, fireEvent, + act, render, waitFor, fireEvent, within, } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { IntlProvider } from '@edx/frontend-platform/i18n'; @@ -12,12 +12,16 @@ import { cloneDeep, set } from 'lodash'; import { getCourseSectionVerticalApiUrl, getCourseUnitApiUrl, + getCourseVerticalChildrenApiUrl, getXBlockBaseApiUrl, postXBlockBaseApiUrl, } from './data/api'; import { + deleteUnitItemQuery, + editCourseUnitVisibilityAndData, fetchCourseSectionVerticalData, fetchCourseUnitQuery, + fetchCourseVerticalChildrenData, } from './data/thunk'; import initializeStore from '../store'; import { @@ -25,13 +29,21 @@ import { courseSectionVerticalMock, courseUnitIndexMock, courseUnitMock, + courseVerticalChildrenMock, } from './__mocks__'; import { executeThunk } from '../utils'; -import CourseUnit from './CourseUnit'; import headerNavigationsMessages from './header-navigations/messages'; import headerTitleMessages from './header-title/messages'; import courseSequenceMessages from './course-sequence/messages'; -import messages from './add-component/messages'; +import sidebarMessages from './sidebar/messages'; +import { extractCourseUnitId } from './sidebar/utils'; +import CourseUnit from './CourseUnit'; +import messages from './messages'; + +import deleteModalMessages from '../generic/delete-modal/messages'; +import courseXBlockMessages from './course-xblock/messages'; +import addComponentMessages from './add-component/messages'; +import { PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants'; let axiosMock; let store; @@ -39,6 +51,7 @@ const courseId = '123'; const blockId = '567890'; const unitDisplayName = courseUnitIndexMock.metadata.display_name; const mockedUsedNavigate = jest.fn(); +const userName = 'openedx'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -75,6 +88,10 @@ describe('', () => { .onGet(getCourseSectionVerticalApiUrl(blockId)) .reply(200, courseSectionVerticalMock); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); + axiosMock + .onGet(getCourseVerticalChildrenApiUrl(blockId)) + .reply(200, courseVerticalChildrenMock); + await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); }); it('render CourseUnit component correctly', async () => { @@ -169,7 +186,7 @@ describe('', () => { await waitFor(() => { const videoButton = getByRole('button', { - name: new RegExp(`${messages.buttonText.defaultMessage} Video`, 'i'), + name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'), }); userEvent.click(videoButton); @@ -182,17 +199,63 @@ describe('', () => { axiosMock .onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId })) .reply(200, courseCreateXblockMock); - const { getByRole } = render(); + const { getByText, getByRole } = render(); + + await waitFor(() => { + userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })); + }); + + axiosMock + .onPost(getXBlockBaseApiUrl(blockId), { + publish: PUBLISH_TYPES.makePublic, + }) + .reply(200, { dummy: 'value' }); + axiosMock + .onGet(getCourseUnitApiUrl(blockId)) + .reply(200, { + ...courseUnitIndexMock, + visibility_state: UNIT_VISIBILITY_STATES.live, + has_changes: false, + published_by: userName, + }); + + await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await waitFor(() => { const problemButton = getByRole('button', { - name: new RegExp(`${messages.buttonText.defaultMessage} Problem`, 'i'), + name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'), }); userEvent.click(problemButton); expect(mockedUsedNavigate).toHaveBeenCalled(); expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/problem/${locator}`); }); + + axiosMock + .onGet(getCourseUnitApiUrl(blockId)) + .reply(200, courseUnitIndexMock); + + await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); + + // after creating problem xblock, the sidebar status changes to Draft (unpublished changes) + expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); + expect(getByText( + sidebarMessages.publishInfoDraftSaved.defaultMessage + .replace('{editedOn}', courseUnitIndexMock.edited_on) + .replace('{editedBy}', courseUnitIndexMock.edited_by), + )).toBeInTheDocument(); + expect(getByText( + sidebarMessages.releaseInfoWithSection.defaultMessage + .replace('{sectionName}', courseUnitIndexMock.release_date_from), + )).toBeInTheDocument(); }); it('correct addition of a new course unit after click on the "Add new unit" button', async () => { @@ -287,16 +350,555 @@ describe('', () => { axiosMock .onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId })) .reply(200, courseCreateXblockMock); - const { getByRole } = render(); + const { getByText, queryByRole, getByRole } = render(); await waitFor(() => { + userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })); + }); + + axiosMock + .onPost(getXBlockBaseApiUrl(blockId), { + publish: PUBLISH_TYPES.makePublic, + }) + .reply(200, { dummy: 'value' }); + axiosMock + .onGet(getCourseUnitApiUrl(blockId)) + .reply(200, { + ...courseUnitIndexMock, + visibility_state: UNIT_VISIBILITY_STATES.live, + has_changes: false, + published_by: userName, + }); + + await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); + + await waitFor(() => { + // check if the sidebar status is Published and Live + expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument(); + expect(getByText( + sidebarMessages.publishLastPublished.defaultMessage + .replace('{publishedOn}', courseUnitIndexMock.published_on) + .replace('{publishedBy}', userName), + )).toBeInTheDocument(); + expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument(); + const videoButton = getByRole('button', { - name: new RegExp(`${messages.buttonText.defaultMessage} Video`, 'i'), + name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'), }); userEvent.click(videoButton); expect(mockedUsedNavigate).toHaveBeenCalled(); expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`); }); + + axiosMock + .onGet(getCourseUnitApiUrl(blockId)) + .reply(200, courseUnitIndexMock); + + await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); + + // after creating video xblock, the sidebar status changes to Draft (unpublished changes) + expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); + expect(getByText( + sidebarMessages.publishInfoDraftSaved.defaultMessage + .replace('{editedOn}', courseUnitIndexMock.edited_on) + .replace('{editedBy}', courseUnitIndexMock.edited_by), + )).toBeInTheDocument(); + expect(getByText( + sidebarMessages.releaseInfoWithSection.defaultMessage + .replace('{sectionName}', courseUnitIndexMock.release_date_from), + )).toBeInTheDocument(); + }); + + it('renders course unit details for a draft with unpublished changes', async () => { + const { getByText } = render(); + + await waitFor(() => { + expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); + expect(getByText( + sidebarMessages.publishInfoDraftSaved.defaultMessage + .replace('{editedOn}', courseUnitIndexMock.edited_on) + .replace('{editedBy}', courseUnitIndexMock.edited_by), + )).toBeInTheDocument(); + expect(getByText( + sidebarMessages.releaseInfoWithSection.defaultMessage + .replace('{sectionName}', courseUnitIndexMock.release_date_from), + )).toBeInTheDocument(); + }); + }); + + it('renders course unit details in the sidebar', async () => { + const { getByText } = render(); + const courseUnitLocationId = extractCourseUnitId(courseUnitIndexMock.id); + + await waitFor(() => { + expect(getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(courseUnitLocationId)).toBeInTheDocument(); + expect(getByText(sidebarMessages.unitLocationDescription.defaultMessage + .replace('{id}', courseUnitLocationId))).toBeInTheDocument(); + }); + }); + + it('should display a warning alert for unpublished course unit version', async () => { + const { getByRole } = render(); + + await waitFor(() => { + const unpublishedAlert = getByRole('alert', { class: 'course-unit-unpublished-alert' }); + expect(unpublishedAlert).toHaveTextContent(messages.alertUnpublishedVersion.defaultMessage); + expect(unpublishedAlert).toHaveClass('alert-warning'); + }); + }); + + it('should not display an unpublished alert for a course unit with explicit staff lock and unpublished status', async () => { + const { queryByRole } = render(); + + axiosMock + .onGet(getCourseUnitApiUrl(courseId)) + .reply(200, { + ...courseUnitIndexMock, + currently_visible_to_students: false, + }); + + await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); + + await waitFor(() => { + const unpublishedAlert = queryByRole('alert', { class: 'course-unit-unpublished-alert' }); + expect(unpublishedAlert).toBeNull(); + }); + }); + + it('checks whether xblock is deleted when corresponding delete button is clicked', async () => { + axiosMock + .onDelete(getXBlockBaseApiUrl(courseVerticalChildrenMock.children[0].block_id)) + .replyOnce(200, { dummy: 'value' }); + + const { + getByText, + getAllByLabelText, + getByRole, + getAllByTestId, + } = render(); + + await waitFor(() => { + expect(getByText(unitDisplayName)).toBeInTheDocument(); + const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage); + userEvent.click(xblockActionBtn); + + const deleteBtn = getByRole('button', { name: courseXBlockMessages.blockLabelButtonDelete.defaultMessage }); + userEvent.click(deleteBtn); + expect(getByText(/Delete this component?/)).toBeInTheDocument(); + + const deleteConfirmBtn = getByRole('button', { name: deleteModalMessages.deleteButton.defaultMessage }); + userEvent.click(deleteConfirmBtn); + + expect(getAllByTestId('course-xblock')).toHaveLength(1); + }); + }); + + it('checks whether xblock is duplicate when corresponding delete button is clicked', async () => { + axiosMock + .onPost(postXBlockBaseApiUrl({ + parent_locator: blockId, + duplicate_source_locator: courseVerticalChildrenMock.children[0].block_id, + })) + .replyOnce(200, { locator: '1234567890' }); + + axiosMock + .onGet(getCourseVerticalChildrenApiUrl(blockId)) + .reply(200, { + ...courseVerticalChildrenMock, + children: [ + ...courseVerticalChildrenMock.children, + { + name: 'New Cloned XBlock', + block_id: '1234567890', + block_type: 'drag-and-drop-v2', + }, + ], + }); + + const { + getByText, + getAllByLabelText, + getAllByTestId, + } = render(); + + await waitFor(() => { + expect(getByText(unitDisplayName)).toBeInTheDocument(); + const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage); + userEvent.click(xblockActionBtn); + + const duplicateBtn = getByText(courseXBlockMessages.blockLabelButtonDuplicate.defaultMessage); + userEvent.click(duplicateBtn); + + expect(getAllByTestId('course-xblock')).toHaveLength(3); + expect(getByText('New Cloned XBlock')).toBeInTheDocument(); + }); + }); + + it('should toggle visibility and update course unit state accordingly', async () => { + const { getByRole, getByTestId } = render(); + let courseUnitSidebar; + let draftUnpublishedChangesHeading; + let visibilityCheckbox; + + await waitFor(() => { + courseUnitSidebar = getByTestId('course-unit-sidebar'); + + draftUnpublishedChangesHeading = within(courseUnitSidebar) + .getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage); + expect(draftUnpublishedChangesHeading).toBeInTheDocument(); + + visibilityCheckbox = within(courseUnitSidebar) + .getByLabelText(sidebarMessages.visibilityCheckboxTitle.defaultMessage); + expect(visibilityCheckbox).not.toBeChecked(); + + userEvent.click(visibilityCheckbox); + }); + + axiosMock + .onPost(getXBlockBaseApiUrl(blockId), { + publish: PUBLISH_TYPES.republish, + metadata: { visible_to_staff_only: true }, + }) + .reply(200, { dummy: 'value' }); + axiosMock + .onGet(getCourseUnitApiUrl(blockId)) + .reply(200, { + ...courseUnitIndexMock, + visibility_state: UNIT_VISIBILITY_STATES.staffOnly, + has_explicit_staff_lock: true, + }); + + await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, true), store.dispatch); + + expect(visibilityCheckbox).toBeChecked(); + expect(within(courseUnitSidebar) + .getByText(sidebarMessages.sidebarTitleVisibleToStaffOnly.defaultMessage)).toBeInTheDocument(); + expect(within(courseUnitSidebar) + .getByText(sidebarMessages.visibilityStaffOnlyTitle.defaultMessage)).toBeInTheDocument(); + + userEvent.click(visibilityCheckbox); + + const modalNotification = 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' }); + + expect(makeVisibilityBtn).toBeInTheDocument(); + expect(cancelBtn).toBeInTheDocument(); + expect(headingElement).toBeInTheDocument(); + expect(within(modalNotification) + .getByText(sidebarMessages.modalMakeVisibilityDescription.defaultMessage)).toBeInTheDocument(); + + userEvent.click(makeVisibilityBtn); + + axiosMock + .onPost(getXBlockBaseApiUrl(blockId), { + publish: PUBLISH_TYPES.republish, + metadata: { visible_to_staff_only: null }, + }) + .reply(200, { dummy: 'value' }); + axiosMock + .onGet(getCourseUnitApiUrl(blockId)) + .reply(200, courseUnitIndexMock); + + 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(); + }); + + it('should publish course unit after click on the "Publish" button', async () => { + const { getByTestId } = render(); + let courseUnitSidebar; + let publishBtn; + + await waitFor(() => { + courseUnitSidebar = getByTestId('course-unit-sidebar'); + publishBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }); + expect(publishBtn).toBeInTheDocument(); + + userEvent.click(publishBtn); + }); + + axiosMock + .onPost(getXBlockBaseApiUrl(blockId), { + publish: PUBLISH_TYPES.makePublic, + }) + .reply(200, { dummy: 'value' }); + axiosMock + .onGet(getCourseUnitApiUrl(blockId)) + .reply(200, { + ...courseUnitIndexMock, + visibility_state: UNIT_VISIBILITY_STATES.live, + has_changes: false, + published_by: userName, + }); + + await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); + + expect(within(courseUnitSidebar) + .getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument(); + expect(within(courseUnitSidebar).getByText( + sidebarMessages.publishLastPublished.defaultMessage + .replace('{publishedOn}', courseUnitIndexMock.published_on) + .replace('{publishedBy}', userName), + )).toBeInTheDocument(); + expect(publishBtn).not.toBeInTheDocument(); + }); + + it('should discard changes after click on the "Discard changes" button', async () => { + const { getByTestId, getByRole } = render(); + let courseUnitSidebar; + let discardChangesBtn; + + await waitFor(() => { + courseUnitSidebar = getByTestId('course-unit-sidebar'); + + const draftUnpublishedChangesHeading = within(courseUnitSidebar) + .getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage); + expect(draftUnpublishedChangesHeading).toBeInTheDocument(); + discardChangesBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage }); + expect(discardChangesBtn).toBeInTheDocument(); + + userEvent.click(discardChangesBtn); + + const modalNotification = getByRole('dialog'); + expect(modalNotification).toBeInTheDocument(); + expect(within(modalNotification) + .getByText(sidebarMessages.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' }); + expect(headingElement).toBeInTheDocument(); + const actionBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalDiscardUnitChangesActionButtonText.defaultMessage }); + expect(actionBtn).toBeInTheDocument(); + + userEvent.click(actionBtn); + }); + + axiosMock + .onPost(getXBlockBaseApiUrl(blockId), { + publish: PUBLISH_TYPES.discardChanges, + }) + .reply(200, { dummy: 'value' }); + axiosMock + .onGet(getCourseUnitApiUrl(blockId)) + .reply(200, { + ...courseUnitIndexMock, published: true, has_changes: false, + }); + + await executeThunk(editCourseUnitVisibilityAndData( + blockId, + PUBLISH_TYPES.discardChanges, + true, + ), store.dispatch); + + expect(within(courseUnitSidebar) + .getByText(sidebarMessages.sidebarTitlePublishedNotYetReleased.defaultMessage)).toBeInTheDocument(); + expect(discardChangesBtn).not.toBeInTheDocument(); + }); + + it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => { + const { + getByText, + getAllByLabelText, + getByRole, + getAllByTestId, + queryByRole, + } = render(); + + await waitFor(() => { + userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })); + }); + + axiosMock + .onPost(getXBlockBaseApiUrl(blockId), { + publish: PUBLISH_TYPES.makePublic, + }) + .reply(200, { dummy: 'value' }); + axiosMock + .onGet(getCourseUnitApiUrl(blockId)) + .reply(200, { + ...courseUnitIndexMock, + visibility_state: UNIT_VISIBILITY_STATES.live, + has_changes: false, + published_by: userName, + }); + + await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); + + axiosMock + .onDelete(getXBlockBaseApiUrl(courseVerticalChildrenMock.children[0].block_id)) + .replyOnce(200, { dummy: 'value' }); + + await executeThunk(deleteUnitItemQuery(courseId, blockId), store.dispatch); + + await waitFor(() => { + // check if the sidebar status is Published and Live + expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument(); + expect(getByText( + sidebarMessages.publishLastPublished.defaultMessage + .replace('{publishedOn}', courseUnitIndexMock.published_on) + .replace('{publishedBy}', userName), + )).toBeInTheDocument(); + expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument(); + + expect(getByText(unitDisplayName)).toBeInTheDocument(); + const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage); + userEvent.click(xblockActionBtn); + + const deleteBtn = getByRole('button', { name: courseXBlockMessages.blockLabelButtonDelete.defaultMessage }); + userEvent.click(deleteBtn); + expect(getByText(/Delete this component?/)).toBeInTheDocument(); + + const deleteConfirmBtn = getByRole('button', { name: deleteModalMessages.deleteButton.defaultMessage }); + userEvent.click(deleteConfirmBtn); + + expect(getAllByTestId('course-xblock')).toHaveLength(1); + }); + + axiosMock + .onGet(getCourseUnitApiUrl(blockId)) + .reply(200, courseUnitIndexMock); + + await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); + + // after removing the xblock, the sidebar status changes to Draft (unpublished changes) + expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); + expect(getByText( + sidebarMessages.publishInfoDraftSaved.defaultMessage + .replace('{editedOn}', courseUnitIndexMock.edited_on) + .replace('{editedBy}', courseUnitIndexMock.edited_by), + )).toBeInTheDocument(); + expect(getByText( + sidebarMessages.releaseInfoWithSection.defaultMessage + .replace('{sectionName}', courseUnitIndexMock.release_date_from), + )).toBeInTheDocument(); + }); + + it('checks if xblock is a duplicate when the corresponding duplicate button is clicked and if the sidebar status is updated', async () => { + axiosMock + .onPost(postXBlockBaseApiUrl({ + parent_locator: blockId, + duplicate_source_locator: courseVerticalChildrenMock.children[0].block_id, + })) + .replyOnce(200, { locator: '1234567890' }); + + axiosMock + .onGet(getCourseVerticalChildrenApiUrl(blockId)) + .reply(200, { + ...courseVerticalChildrenMock, + children: [ + ...courseVerticalChildrenMock.children, + { + ...courseVerticalChildrenMock.children[0], + name: 'New Cloned XBlock', + }, + ], + }); + + const { + getByText, + getAllByLabelText, + getAllByTestId, + queryByRole, + getByRole, + } = render(); + + await waitFor(() => { + userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })); + }); + + axiosMock + .onPost(getXBlockBaseApiUrl(blockId), { + publish: PUBLISH_TYPES.makePublic, + }) + .reply(200, { dummy: 'value' }); + axiosMock + .onGet(getCourseUnitApiUrl(blockId)) + .reply(200, { + ...courseUnitIndexMock, + visibility_state: UNIT_VISIBILITY_STATES.live, + has_changes: false, + published_by: userName, + }); + + await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); + + await waitFor(() => { + // check if the sidebar status is Published and Live + expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument(); + expect(getByText( + sidebarMessages.publishLastPublished.defaultMessage + .replace('{publishedOn}', courseUnitIndexMock.published_on) + .replace('{publishedBy}', userName), + )).toBeInTheDocument(); + expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument(); + + expect(getByText(unitDisplayName)).toBeInTheDocument(); + const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage); + userEvent.click(xblockActionBtn); + + const duplicateBtn = getByText(courseXBlockMessages.blockLabelButtonDuplicate.defaultMessage); + userEvent.click(duplicateBtn); + + expect(getAllByTestId('course-xblock')).toHaveLength(3); + expect(getByText('New Cloned XBlock')).toBeInTheDocument(); + }); + + axiosMock + .onGet(getCourseUnitApiUrl(blockId)) + .reply(200, courseUnitIndexMock); + + await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); + + // after duplicate the xblock, the sidebar status changes to Draft (unpublished changes) + expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); + expect(getByText( + sidebarMessages.publishInfoDraftSaved.defaultMessage + .replace('{editedOn}', courseUnitIndexMock.edited_on) + .replace('{editedBy}', courseUnitIndexMock.edited_by), + )).toBeInTheDocument(); + expect(getByText( + sidebarMessages.releaseInfoWithSection.defaultMessage + .replace('{sectionName}', courseUnitIndexMock.release_date_from), + )).toBeInTheDocument(); }); }); diff --git a/src/course-unit/__mocks__/courseVerticalChildren.js b/src/course-unit/__mocks__/courseVerticalChildren.js new file mode 100644 index 0000000000..d7cc9bf611 --- /dev/null +++ b/src/course-unit/__mocks__/courseVerticalChildren.js @@ -0,0 +1,15 @@ +module.exports = { + children: [ + { + name: 'Discussion', + block_id: 'block-v1:OpenedX+L153+3T2023+type@discussion+block@fecd20842dd24f50bdc06643e791b013', + block_type: 'discussion', + }, + { + name: 'Drag and Drop', + block_id: 'block-v1:OpenedX+L153+3T2023+type@drag-and-drop-v2+block@b33cf1f6df4c41639659bc91132eeb02', + block_type: 'drag-and-drop-v2', + }, + ], + is_published: false, +}; diff --git a/src/course-unit/__mocks__/index.js b/src/course-unit/__mocks__/index.js index cd5c4ffdfe..d8c220b7a4 100644 --- a/src/course-unit/__mocks__/index.js +++ b/src/course-unit/__mocks__/index.js @@ -2,3 +2,4 @@ export { default as courseUnitIndexMock } from './courseUnitIndex'; export { default as courseSectionVerticalMock } from './courseSectionVertical'; export { default as courseUnitMock } from './courseUnit'; export { default as courseCreateXblockMock } from './courseCreateXblock'; +export { default as courseVerticalChildrenMock } from './courseVerticalChildren'; diff --git a/src/course-unit/constants.js b/src/course-unit/constants.js index 3786de4a10..1351689a36 100644 --- a/src/course-unit/constants.js +++ b/src/course-unit/constants.js @@ -12,6 +12,7 @@ import { TextFields as TextFieldsIcon, VideoCamera as VideoCameraIcon, } from '@openedx/paragon/icons'; +import messages from './sidebar/messages'; export const UNIT_ICON_TYPES = ['video', 'other', 'vertical', 'problem', 'lock']; @@ -44,3 +45,26 @@ export const COMPONENT_TYPE_ICON_MAP = { [COMPONENT_ICON_TYPES.video]: VideoCameraIcon, [COMPONENT_ICON_TYPES.dragAndDrop]: BackHandIcon, }; + +export const getUnitReleaseStatus = (intl) => ({ + release: intl.formatMessage(messages.releaseStatusTitle), + released: intl.formatMessage(messages.releasedStatusTitle), + scheduled: intl.formatMessage(messages.scheduledStatusTitle), +}); + +export const UNIT_VISIBILITY_STATES = { + staffOnly: 'staff_only', + live: 'live', + ready: 'ready', +}; + +export const ICON_COLOR_VARIANTS = { + BLACK: '#000', + GREEN: '#0D7D4D', +}; + +export const PUBLISH_TYPES = { + republish: 'republish', + discardChanges: 'discard_changes', + makePublic: 'make_public', +}; diff --git a/src/course-unit/course-xblock/CourseXBlock.jsx b/src/course-unit/course-xblock/CourseXBlock.jsx new file mode 100644 index 0000000000..bd8a11ebfc --- /dev/null +++ b/src/course-unit/course-xblock/CourseXBlock.jsx @@ -0,0 +1,104 @@ +import { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { + ActionRow, Card, Dropdown, Icon, IconButton, useToggle, +} from '@openedx/paragon'; +import { EditOutline as EditIcon, MoreVert as MoveVertIcon } from '@openedx/paragon/icons'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import DeleteModal from '../../generic/delete-modal/DeleteModal'; +import { scrollToElement } from '../../course-outline/utils'; +import messages from './messages'; + +const CourseXBlock = ({ + id, title, unitXBlockActions, shouldScroll, ...props +}) => { + const courseXBlockElementRef = useRef(null); + const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false); + const intl = useIntl(); + + const onXBlockDelete = () => { + unitXBlockActions.handleDelete(id); + closeDeleteModal(); + }; + + useEffect(() => { + // if this item has been newly added, scroll to it. + if (courseXBlockElementRef.current && shouldScroll) { + scrollToElement(courseXBlockElementRef.current); + } + }, []); + + return ( +
+ + + {}} + /> + + + + + {intl.formatMessage(messages.blockLabelButtonCopy)} + + unitXBlockActions.handleDuplicate(id)}> + {intl.formatMessage(messages.blockLabelButtonDuplicate)} + + + {intl.formatMessage(messages.blockLabelButtonMove)} + + + {intl.formatMessage(messages.blockLabelButtonManageAccess)} + + + {intl.formatMessage(messages.blockLabelButtonDelete)} + + + + + + )} + size="md" + /> + +
+ + +
+ ); +}; + +CourseXBlock.defaultProps = { + shouldScroll: false, +}; + +CourseXBlock.propTypes = { + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + shouldScroll: PropTypes.bool, + unitXBlockActions: PropTypes.shape({ + handleDelete: PropTypes.func, + handleDuplicate: PropTypes.func, + }).isRequired, +}; + +export default CourseXBlock; diff --git a/src/course-unit/course-xblock/CourseXBlock.scss b/src/course-unit/course-xblock/CourseXBlock.scss new file mode 100644 index 0000000000..52c8e0bef5 --- /dev/null +++ b/src/course-unit/course-xblock/CourseXBlock.scss @@ -0,0 +1,15 @@ +.course-unit { + .pgn__card .pgn__card-header { + border-bottom: 1px solid $light-400; + padding-bottom: map-get($spacers, 2); + + .pgn__card-header-content { + margin-top: map-get($spacers, 3\.5); + } + + .btn-icon .btn-icon__icon { + width: 1.5rem; + height: 1.5rem; + } + } +} diff --git a/src/course-unit/course-xblock/CourseXBlock.test.jsx b/src/course-unit/course-xblock/CourseXBlock.test.jsx new file mode 100644 index 0000000000..2c6defc766 --- /dev/null +++ b/src/course-unit/course-xblock/CourseXBlock.test.jsx @@ -0,0 +1,108 @@ +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { AppProvider } from '@edx/frontend-platform/react'; + +import { courseVerticalChildrenMock } from '../__mocks__'; +import CourseXBlock from './CourseXBlock'; + +import deleteModalMessages from '../../generic/delete-modal/messages'; +import messages from './messages'; + +let store; +const handleDeleteMock = jest.fn(); +const handleDuplicateMock = jest.fn(); +const xblockData = courseVerticalChildrenMock.children[0]; +const unitXBlockActionsMock = { + handleDelete: handleDeleteMock, + handleDuplicate: handleDuplicateMock, +}; + +const renderComponent = () => render( + + + + + , +); + +describe('', () => { + beforeEach(async () => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + }); + + it('render CourseXBlock component correctly', async () => { + const { getByText, getByLabelText } = renderComponent(); + + await waitFor(() => { + expect(getByText(xblockData.block_id)).toBeInTheDocument(); + expect(getByLabelText(messages.blockAltButtonEdit.defaultMessage)).toBeInTheDocument(); + expect(getByLabelText(messages.blockActionsDropdownAlt.defaultMessage)).toBeInTheDocument(); + }); + }); + + it('render CourseXBlock component action dropdown correctly', async () => { + const { getByRole, getByLabelText } = renderComponent(); + + await waitFor(() => { + userEvent.click(getByLabelText(messages.blockActionsDropdownAlt.defaultMessage)); + expect(getByRole('button', { name: messages.blockLabelButtonCopy.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.blockLabelButtonDuplicate.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.blockLabelButtonMove.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.blockLabelButtonManageAccess.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.blockLabelButtonDelete.defaultMessage })).toBeInTheDocument(); + }); + }); + + it('calls handleDuplicate when item is clicked', async () => { + const { getByText, getByLabelText } = renderComponent(); + + await waitFor(() => { + userEvent.click(getByLabelText(messages.blockActionsDropdownAlt.defaultMessage)); + const duplicateBtn = getByText(messages.blockLabelButtonDuplicate.defaultMessage); + + userEvent.click(duplicateBtn); + expect(handleDuplicateMock).toHaveBeenCalledTimes(1); + expect(handleDuplicateMock).toHaveBeenCalledWith(xblockData.block_id); + }); + }); + + it('opens confirm delete modal and calls handleDelete when deleting was confirmed', async () => { + const { getByText, getByLabelText, getByRole } = renderComponent(); + + await waitFor(() => { + userEvent.click(getByLabelText(messages.blockActionsDropdownAlt.defaultMessage)); + const deleteBtn = getByText(messages.blockLabelButtonDelete.defaultMessage); + + userEvent.click(deleteBtn); + expect(getByText(/Delete this component?/)).toBeInTheDocument(); + expect(getByText(/Deleting this component is permanent and cannot be undone./)).toBeInTheDocument(); + expect(getByRole('button', { name: deleteModalMessages.cancelButton.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: deleteModalMessages.deleteButton.defaultMessage })).toBeInTheDocument(); + + userEvent.click(getByRole('button', { name: deleteModalMessages.cancelButton.defaultMessage })); + expect(handleDeleteMock).not.toHaveBeenCalled(); + + userEvent.click(getByText(messages.blockLabelButtonDelete.defaultMessage)); + expect(getByText(/Delete this component?/)).toBeInTheDocument(); + + userEvent.click(deleteBtn); + userEvent.click(getByRole('button', { name: deleteModalMessages.deleteButton.defaultMessage })); + expect(handleDeleteMock).toHaveBeenCalled(); + expect(handleDeleteMock).toHaveBeenCalledWith(xblockData.block_id); + }); + }); +}); diff --git a/src/course-unit/course-xblock/messages.js b/src/course-unit/course-xblock/messages.js new file mode 100644 index 0000000000..80e25dac13 --- /dev/null +++ b/src/course-unit/course-xblock/messages.js @@ -0,0 +1,34 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + blockAltButtonEdit: { + id: 'course-authoring.course-unit.xblock.button.edit.alt', + defaultMessage: 'Edit Item', + }, + blockActionsDropdownAlt: { + id: 'course-authoring.course-unit.xblock.button.actions.alt', + defaultMessage: 'Actions', + }, + blockLabelButtonCopy: { + id: 'course-authoring.course-unit.xblock.button.copy.label', + defaultMessage: 'Copy', + }, + blockLabelButtonDuplicate: { + id: 'course-authoring.course-unit.xblock.button.duplicate.label', + defaultMessage: 'Duplicate', + }, + blockLabelButtonMove: { + id: 'course-authoring.course-unit.xblock.button.move.label', + defaultMessage: 'Move', + }, + blockLabelButtonManageAccess: { + id: 'course-authoring.course-unit.xblock.button.manageAccess.label', + defaultMessage: 'Manage access', + }, + blockLabelButtonDelete: { + id: 'course-authoring.course-unit.xblock.button.delete.label', + defaultMessage: 'Delete', + }, +}); + +export default messages; diff --git a/src/course-unit/data/api.js b/src/course-unit/data/api.js index 5e1e5158a6..f7adc33b47 100644 --- a/src/course-unit/data/api.js +++ b/src/course-unit/data/api.js @@ -2,6 +2,7 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { PUBLISH_TYPES } from '../constants'; import { normalizeLearningSequencesData, normalizeMetadata, @@ -19,6 +20,7 @@ export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()} export const getLearningSequencesOutlineApiUrl = (courseId) => `${getLmsBaseUrl()}/api/learning_sequences/v1/course_outline/${courseId}`; export const getCourseMetadataApiUrl = (courseId) => `${getLmsBaseUrl()}/api/courseware/course/${courseId}`; export const getCourseHomeCourseMetadataApiUrl = (courseId) => `${getLmsBaseUrl()}/api/course_home/course_metadata/${courseId}`; +export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`; export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`; @@ -128,3 +130,67 @@ export async function createCourseXblock({ return data; } + +/** + * Handles the visibility and data of a course unit, such as publishing, resetting to default values, + * and toggling visibility to students. + * @param {string} unitId - The ID of the course unit. + * @param {string} type - The action type (e.g., PUBLISH_TYPES.discardChanges). + * @param {boolean} isVisible - The visibility status for students. + * @returns {Promise} A promise that resolves with the response data. + */ +export async function handleCourseUnitVisibilityAndData(unitId, type, isVisible) { + const body = { + publish: type, + ...(type === PUBLISH_TYPES.republish ? { + metadata: { + visible_to_staff_only: isVisible, + }, + } : {}), + }; + + const { data } = await getAuthenticatedHttpClient() + .post(getXBlockBaseApiUrl(unitId), body); + + return camelCaseObject(data); +} + +/** + * Get an object containing course section vertical children data. + * @param {string} itemId + * @returns {Promise} + */ +export async function getCourseVerticalChildren(itemId) { + const { data } = await getAuthenticatedHttpClient() + .get(getCourseVerticalChildrenApiUrl(itemId)); + + return camelCaseObject(data); +} + +/** + * Delete a unit item. + * @param {string} itemId + * @returns {Promise} + */ +export async function deleteUnitItem(itemId) { + const { data } = await getAuthenticatedHttpClient() + .delete(getXBlockBaseApiUrl(itemId)); + + return data; +} + +/** + * Duplicate a unit item. + * @param {string} itemId + * @param {string} XBlockId + * @returns {Promise} + */ +export async function duplicateUnitItem(itemId, XBlockId) { + const { data } = await getAuthenticatedHttpClient() + .post(postXBlockBaseApiUrl(), { + parent_locator: itemId, + duplicate_source_locator: XBlockId, + }); + + return data; +} diff --git a/src/course-unit/data/selectors.js b/src/course-unit/data/selectors.js index a5a1eb95b6..5fa52ac1b4 100644 --- a/src/course-unit/data/selectors.js +++ b/src/course-unit/data/selectors.js @@ -16,6 +16,7 @@ export const getCoursewareMeta = state => state.models.coursewareMeta; export const getSections = state => state.models.sections; export const getCourseId = state => state.courseDetail.courseId; export const getSequenceId = state => state.courseUnit.sequenceId; +export const getCourseVerticalChildren = state => state.courseUnit.courseVerticalChildren; export const sequenceIdsSelector = createSelector( [getCourseStatus, getCoursewareMeta, getSections, getCourseId], (courseStatus, coursewareMeta, sections, courseId) => { diff --git a/src/course-unit/data/slice.js b/src/course-unit/data/slice.js index 7f243c5110..bd4066afcc 100644 --- a/src/course-unit/data/slice.js +++ b/src/course-unit/data/slice.js @@ -12,9 +12,11 @@ const slice = createSlice({ loadingStatus: { fetchUnitLoadingStatus: RequestStatus.IN_PROGRESS, courseSectionVerticalLoadingStatus: RequestStatus.IN_PROGRESS, + courseVerticalChildrenLoadingStatus: RequestStatus.IN_PROGRESS, }, unit: {}, courseSectionVertical: {}, + courseVerticalChildren: [], }, reducers: { fetchCourseItemSuccess: (state, { payload }) => { @@ -87,6 +89,28 @@ const slice = createSlice({ fetchUnitLoadingStatus: payload.status, }; }, + updateCourseVerticalChildren: (state, { payload }) => { + state.courseVerticalChildren = payload; + }, + updateCourseVerticalChildrenLoadingStatus: (state, { payload }) => { + state.loadingStatus.courseVerticalChildrenLoadingStatus = payload.status; + }, + deleteXBlock: (state, { payload }) => { + state.courseVerticalChildren.children = state.courseVerticalChildren.children.filter( + (component) => component.blockId !== payload, + ); + }, + duplicateXBlock: (state, { payload }) => { + state.courseVerticalChildren = { + ...payload.newCourseVerticalChildren, + children: payload.newCourseVerticalChildren.children.map((component) => { + if (component.blockId === payload.newId) { + component.shouldScroll = true; + } + return component; + }), + }; + }, }, }); @@ -107,6 +131,10 @@ export const { changeEditTitleFormOpen, updateQueryPendingStatus, updateLoadingCourseXblockStatus, + updateCourseVerticalChildren, + updateCourseVerticalChildrenLoadingStatus, + deleteXBlock, + duplicateXBlock, } = slice.actions; export const { diff --git a/src/course-unit/data/thunk.js b/src/course-unit/data/thunk.js index defb940cd6..23b39937a1 100644 --- a/src/course-unit/data/thunk.js +++ b/src/course-unit/data/thunk.js @@ -1,4 +1,5 @@ import { logError, logInfo } from '@edx/frontend-platform/logging'; +import { camelCaseObject } from '@edx/frontend-platform'; import { hideProcessingNotification, @@ -17,6 +18,10 @@ import { getCourseHomeCourseMetadata, getCourseSectionVerticalData, createCourseXblock, + getCourseVerticalChildren, + handleCourseUnitVisibilityAndData, + deleteUnitItem, + duplicateUnitItem, } from './api'; import { updateLoadingCourseUnitStatus, @@ -32,7 +37,13 @@ import { fetchCourseSectionVerticalDataSuccess, updateLoadingCourseSectionVerticalDataStatus, updateLoadingCourseXblockStatus, + updateCourseVerticalChildren, + updateCourseVerticalChildrenLoadingStatus, + updateQueryPendingStatus, + deleteXBlock, + duplicateXBlock, } from './slice'; +import { getNotificationMessage } from './utils'; export function fetchCourseUnitQuery(courseId) { return async (dispatch) => { @@ -110,6 +121,31 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) { }; } +export function editCourseUnitVisibilityAndData(itemId, type, isVisible) { + return async (dispatch) => { + dispatch(updateSavingStatus({ status: RequestStatus.PENDING })); + dispatch(updateQueryPendingStatus(true)); + const notificationMessage = getNotificationMessage(type, isVisible); + dispatch(showProcessingNotification(notificationMessage)); + + try { + await handleCourseUnitVisibilityAndData(itemId, type, isVisible).then(async (result) => { + if (result) { + const courseUnit = await getCourseUnitData(itemId); + dispatch(fetchCourseItemSuccess(courseUnit)); + const courseVerticalChildrenData = await getCourseVerticalChildren(itemId); + dispatch(updateCourseVerticalChildren(courseVerticalChildrenData)); + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); + } + }); + } catch (error) { + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + } + }; +} + export function fetchCourse(courseId) { return async (dispatch) => { dispatch(fetchCourseRequest({ courseId })); @@ -192,7 +228,7 @@ export function fetchCourse(courseId) { }; } -export function createNewCourseXblock(body, callback) { +export function createNewCourseXBlock(body, callback, blockId) { return async (dispatch) => { dispatch(updateLoadingCourseXblockStatus({ status: RequestStatus.IN_PROGRESS })); dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.adding)); @@ -201,18 +237,25 @@ export function createNewCourseXblock(body, callback) { try { await createCourseXblock(body).then(async (result) => { if (result) { + const formattedResult = camelCaseObject(result); if (body.category === 'vertical') { - const courseSectionVerticalData = await getCourseSectionVerticalData(result.locator); + const courseSectionVerticalData = await getCourseSectionVerticalData(formattedResult.locator); dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData)); } - // ToDo: implement fetching (update) xblocks after success creating + const courseVerticalChildrenData = await getCourseVerticalChildren(blockId); + dispatch(updateCourseVerticalChildren(courseVerticalChildrenData)); dispatch(hideProcessingNotification()); dispatch(updateLoadingCourseXblockStatus({ status: RequestStatus.SUCCESSFUL })); dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); if (callback) { callback(result); } + const currentBlockId = body.category === 'vertical' ? formattedResult.locator : blockId; + const courseUnit = await getCourseUnitData(currentBlockId); + dispatch(fetchCourseItemSuccess(courseUnit)); } + const courseUnit = await getCourseUnitData(blockId); + dispatch(fetchCourseItemSuccess(courseUnit)); }); } catch (error) { dispatch(hideProcessingNotification()); @@ -221,3 +264,59 @@ export function createNewCourseXblock(body, callback) { } }; } + +export function fetchCourseVerticalChildrenData(itemId) { + return async (dispatch) => { + dispatch(updateCourseVerticalChildrenLoadingStatus({ status: RequestStatus.IN_PROGRESS })); + + try { + const courseVerticalChildrenData = await getCourseVerticalChildren(itemId); + dispatch(updateCourseVerticalChildren(courseVerticalChildrenData)); + dispatch(updateCourseVerticalChildrenLoadingStatus({ status: RequestStatus.SUCCESSFUL })); + } catch (error) { + dispatch(updateCourseVerticalChildrenLoadingStatus({ status: RequestStatus.FAILED })); + } + }; +} + +export function deleteUnitItemQuery(itemId, xblockId) { + return async (dispatch) => { + dispatch(updateSavingStatus({ status: RequestStatus.PENDING })); + dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.deleting)); + + try { + await deleteUnitItem(xblockId); + dispatch(deleteXBlock(xblockId)); + const courseUnit = await getCourseUnitData(itemId); + dispatch(fetchCourseItemSuccess(courseUnit)); + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); + } catch (error) { + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + } + }; +} + +export function duplicateUnitItemQuery(itemId, xblockId) { + return async (dispatch) => { + dispatch(updateSavingStatus({ status: RequestStatus.PENDING })); + dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.duplicating)); + + try { + const { locator } = await duplicateUnitItem(itemId, xblockId); + const newCourseVerticalChildren = await getCourseVerticalChildren(itemId); + dispatch(duplicateXBlock({ + newId: locator, + newCourseVerticalChildren, + })); + const courseUnit = await getCourseUnitData(itemId); + dispatch(fetchCourseItemSuccess(courseUnit)); + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); + } catch (error) { + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + } + }; +} diff --git a/src/course-unit/data/utils.js b/src/course-unit/data/utils.js index c41db3a85a..afd6e0a03a 100644 --- a/src/course-unit/data/utils.js +++ b/src/course-unit/data/utils.js @@ -1,5 +1,8 @@ import { camelCaseObject } from '@edx/frontend-platform'; +import { NOTIFICATION_MESSAGES } from '../../constants'; +import { PUBLISH_TYPES } from '../constants'; + export function getTimeOffsetMillis(headerDate, requestTime, responseTime) { // Time offset computation should move down into the HttpClient wrapper to maintain a global time correction reference // Requires 'Access-Control-Expose-Headers: Date' on the server response per https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-expose-headers @@ -211,3 +214,25 @@ export function normalizeCourseSectionVerticalData(metadata) { })), }; } + +/** + * Get the notification message based on the publishing type and visibility. + * @param {string} type - The publishing type. + * @param {boolean} isVisible - The visibility status. + * @returns {string} The corresponding notification message. + */ +export const getNotificationMessage = (type, isVisible) => { + let notificationMessage; + + if (type === PUBLISH_TYPES.discardChanges) { + notificationMessage = NOTIFICATION_MESSAGES.discardChanges; + } else if (type === PUBLISH_TYPES.makePublic) { + notificationMessage = NOTIFICATION_MESSAGES.publishing; + } else if (type === PUBLISH_TYPES.republish && !isVisible) { + notificationMessage = NOTIFICATION_MESSAGES.makingVisibleToStudents; + } else if (type === PUBLISH_TYPES.republish && isVisible) { + notificationMessage = NOTIFICATION_MESSAGES.hidingFromStudents; + } + + return notificationMessage; +}; diff --git a/src/course-unit/hooks.jsx b/src/course-unit/hooks.jsx index 7c7a01fc93..dea7599b89 100644 --- a/src/course-unit/hooks.jsx +++ b/src/course-unit/hooks.jsx @@ -4,14 +4,18 @@ import { useNavigate } from 'react-router-dom'; import { RequestStatus } from '../data/constants'; import { - createNewCourseXblock, + createNewCourseXBlock, fetchCourseUnitQuery, editCourseItemQuery, fetchCourse, fetchCourseSectionVerticalData, + fetchCourseVerticalChildrenData, + deleteUnitItemQuery, + duplicateUnitItemQuery, } from './data/thunk'; import { getCourseSectionVertical, + getCourseVerticalChildren, getCourseUnitData, getLoadingStatus, getSavingStatus, @@ -28,9 +32,11 @@ export const useCourseUnit = ({ courseId, blockId }) => { const savingStatus = useSelector(getSavingStatus); const loadingStatus = useSelector(getLoadingStatus); const { draftPreviewLink, publishedPreviewLink } = useSelector(getCourseSectionVertical); + const courseVerticalChildren = useSelector(getCourseVerticalChildren); const navigate = useNavigate(); const isTitleEditFormOpen = useSelector(state => state.courseUnit.isTitleEditFormOpen); const isQueryPending = useSelector(state => state.courseUnit.isQueryPending); + const { currentlyVisibleToStudents } = courseUnit; const unitTitle = courseUnit.metadata?.displayName || ''; const sequenceId = courseUnit.ancestorInfo?.ancestors[0].id; @@ -67,9 +73,18 @@ export const useCourseUnit = ({ courseId, blockId }) => { }; const handleCreateNewCourseXBlock = (body, callback) => ( - dispatch(createNewCourseXblock(body, callback)) + dispatch(createNewCourseXBlock(body, callback, blockId)) ); + const unitXBlockActions = { + handleDelete: (XBlockId) => { + dispatch(deleteUnitItemQuery(blockId, XBlockId)); + }, + handleDuplicate: (XBlockId) => { + dispatch(duplicateUnitItemQuery(blockId, XBlockId)); + }, + }; + useEffect(() => { if (savingStatus === RequestStatus.SUCCESSFUL) { dispatch(updateQueryPendingStatus(false)); @@ -81,6 +96,7 @@ export const useCourseUnit = ({ courseId, blockId }) => { useEffect(() => { dispatch(fetchCourseUnitQuery(blockId)); dispatch(fetchCourseSectionVerticalData(blockId, sequenceId)); + dispatch(fetchCourseVerticalChildrenData(blockId)); dispatch(fetchCourse(courseId)); handleNavigate(sequenceId); @@ -93,14 +109,17 @@ export const useCourseUnit = ({ courseId, blockId }) => { savingStatus, isQueryPending, isErrorAlert, + currentlyVisibleToStudents, isLoading: loadingStatus.fetchUnitLoadingStatus === RequestStatus.IN_PROGRESS || loadingStatus.courseSectionVerticalLoadingStatus === RequestStatus.IN_PROGRESS, isTitleEditFormOpen, isInternetConnectionAlertFailed: savingStatus === RequestStatus.FAILED, handleInternetConnectionFailed, + unitXBlockActions, headerNavigationsActions, handleTitleEdit, handleTitleEditSubmit, handleCreateNewCourseXBlock, + courseVerticalChildren, }; }; diff --git a/src/course-unit/messages.js b/src/course-unit/messages.js index 42533513e5..ba27b0fb78 100644 --- a/src/course-unit/messages.js +++ b/src/course-unit/messages.js @@ -5,6 +5,10 @@ const messages = defineMessages({ id: 'course-authoring.course-unit.general.alert.error.description', defaultMessage: 'Unable to {actionName} {type}. Please try again.', }, + alertUnpublishedVersion: { + id: 'course-authoring.course-unit.general.alert.unpublished-version.description', + defaultMessage: 'Note: The last published version of this unit is live. By publishing changes you will change the student experience.', + }, }); export default messages; diff --git a/src/course-unit/sidebar/Sidebar.scss b/src/course-unit/sidebar/Sidebar.scss new file mode 100644 index 0000000000..954e20d4b2 --- /dev/null +++ b/src/course-unit/sidebar/Sidebar.scss @@ -0,0 +1,76 @@ +%base-font-params { + font-size: $font-size-sm; + line-height: $line-height-base; +} + +.course-unit-sidebar { + .course-unit-sidebar-header { + padding: $spacer $spacer map-get($spacers, 3\.5); + + .course-unit-sidebar-header-icon { + margin-right: map-get($spacers, 1); + } + + .course-unit-sidebar-header-title { + font-size: $font-size-base; + line-height: $line-height-base; + } + } + + .course-unit-sidebar-footer { + padding: 0 $spacer $spacer; + + .course-unit-sidebar-visibility { + .course-unit-sidebar-visibility-title { + font-weight: $font-weight-normal; + color: $gray-700; + + @extend %base-font-params; + } + + .course-unit-sidebar-visibility-section { + @extend %base-font-params; + } + + .course-unit-sidebar-location-description { + font-size: $font-size-xs; + line-height: $line-height-base; + word-break: break-word; + } + + .course-unit-sidebar-visibility-copy { + font-weight: $font-weight-bold; + color: $gray-700; + + @extend %base-font-params; + } + + .course-unit-sidebar-visibility-checkbox .pgn__form-label { + font-size: $font-size-sm; + line-height: $headings-line-height; + } + } + } + + .course-unit-sidebar-date { + padding: 0 $spacer $spacer; + + @extend %base-font-params; + + .course-unit-sidebar-date-stage { + font-weight: $font-weight-normal; + + @extend %base-font-params; + } + + .course-unit-sidebar-date-timestamp { + color: $gray-700; + + @extend %base-font-params; + } + } + + &.is-stuff-only .course-unit-sidebar-date-and-with { + text-decoration: line-through; + } +} diff --git a/src/course-unit/sidebar/components/ReleaseInfoComponent.jsx b/src/course-unit/sidebar/components/ReleaseInfoComponent.jsx new file mode 100644 index 0000000000..9861fa2094 --- /dev/null +++ b/src/course-unit/sidebar/components/ReleaseInfoComponent.jsx @@ -0,0 +1,29 @@ +import { useSelector } from 'react-redux'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { getCourseUnitData } from '../../data/selectors'; +import { getReleaseInfo } from '../utils'; + +const ReleaseInfoComponent = () => { + const intl = useIntl(); + const { + releaseDate, + releaseDateFrom, + } = useSelector(getCourseUnitData); + const releaseInfo = getReleaseInfo(intl, releaseDate, releaseDateFrom); + + if (releaseInfo.isScheduled) { + return ( + +
+ {releaseInfo.releaseDate}  +
+ {releaseInfo.sectionNameMessage} +
+ ); + } + + return releaseInfo.message; +}; + +export default ReleaseInfoComponent; diff --git a/src/course-unit/sidebar/components/SidebarBody.jsx b/src/course-unit/sidebar/components/SidebarBody.jsx new file mode 100644 index 0000000000..679384377d --- /dev/null +++ b/src/course-unit/sidebar/components/SidebarBody.jsx @@ -0,0 +1,65 @@ +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { Card, Stack } from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { getCourseUnitData } from '../../data/selectors'; +import { getPublishInfo } from '../utils'; +import messages from '../messages'; +import ReleaseInfoComponent from './ReleaseInfoComponent'; + +const SidebarBody = ({ releaseLabel, displayUnitLocation, locationId }) => { + const intl = useIntl(); + const { + editedOn, + editedBy, + hasChanges, + publishedBy, + publishedOn, + } = useSelector(getCourseUnitData); + + return ( + + + {displayUnitLocation ? ( + +
+ {intl.formatMessage(messages.unitLocationTitle)} +
+

+ {locationId} +

+
+ ) : ( + <> + + {getPublishInfo(intl, hasChanges, editedBy, editedOn, publishedBy, publishedOn)} + + +
+ {releaseLabel} +
+ +
+

+ {intl.formatMessage(messages.sidebarBodyNote)} +

+ + )} +
+
+ ); +}; + +SidebarBody.propTypes = { + releaseLabel: PropTypes.string.isRequired, + displayUnitLocation: PropTypes.bool, + locationId: PropTypes.string, +}; + +SidebarBody.defaultProps = { + displayUnitLocation: false, + locationId: null, +}; + +export default SidebarBody; diff --git a/src/course-unit/sidebar/components/SidebarHeader.jsx b/src/course-unit/sidebar/components/SidebarHeader.jsx new file mode 100644 index 0000000000..b6b6feda03 --- /dev/null +++ b/src/course-unit/sidebar/components/SidebarHeader.jsx @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { Icon, Stack } from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { getCourseUnitData } from '../../data/selectors'; +import { getIconVariant } from '../utils'; +import messages from '../messages'; + +const SidebarHeader = ({ title, visibilityState, displayUnitLocation }) => { + const intl = useIntl(); + const { hasChanges, published } = useSelector(getCourseUnitData); + const { iconSrc, colorVariant } = getIconVariant(visibilityState, published, hasChanges); + + return ( + + {!displayUnitLocation && ( + + )} +

+ {displayUnitLocation ? intl.formatMessage(messages.sidebarHeaderUnitLocationTitle) : title} +

+
+ ); +}; + +SidebarHeader.propTypes = { + title: PropTypes.string.isRequired, + visibilityState: PropTypes.string.isRequired, + displayUnitLocation: PropTypes.bool, +}; + +SidebarHeader.defaultProps = { + displayUnitLocation: false, +}; + +export default SidebarHeader; diff --git a/src/course-unit/sidebar/components/index.js b/src/course-unit/sidebar/components/index.js new file mode 100644 index 0000000000..6637529e61 --- /dev/null +++ b/src/course-unit/sidebar/components/index.js @@ -0,0 +1,3 @@ +export { default as SidebarHeader } from './SidebarHeader'; +export { default as SidebarBody } from './SidebarBody'; +export { default as SidebarFooter } from './sidebar-footer'; diff --git a/src/course-unit/sidebar/components/sidebar-footer/ActionButtons.jsx b/src/course-unit/sidebar/components/sidebar-footer/ActionButtons.jsx new file mode 100644 index 0000000000..ac0a63287d --- /dev/null +++ b/src/course-unit/sidebar/components/sidebar-footer/ActionButtons.jsx @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { Button } from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { getCourseUnitData } from '../../../data/selectors'; +import messages from '../../messages'; + +const ActionButtons = ({ openDiscardModal, handlePublishing }) => { + const intl = useIntl(); + const { + published, + hasChanges, + enableCopyPasteUnits, + } = useSelector(getCourseUnitData); + + return ( + <> + {(!published || hasChanges) && ( + + )} + {(published && hasChanges) && ( + + )} + {enableCopyPasteUnits && ( + + )} + + ); +}; + +ActionButtons.propTypes = { + openDiscardModal: PropTypes.func.isRequired, + handlePublishing: PropTypes.func.isRequired, +}; + +export default ActionButtons; diff --git a/src/course-unit/sidebar/components/sidebar-footer/UnitVisibilityInfo.jsx b/src/course-unit/sidebar/components/sidebar-footer/UnitVisibilityInfo.jsx new file mode 100644 index 0000000000..b4dc35b568 --- /dev/null +++ b/src/course-unit/sidebar/components/sidebar-footer/UnitVisibilityInfo.jsx @@ -0,0 +1,67 @@ +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import { Form } from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { useParams } from 'react-router-dom'; + +import { getCourseUnitData } from '../../../data/selectors'; +import { editCourseUnitVisibilityAndData } from '../../../data/thunk'; +import { PUBLISH_TYPES } from '../../../constants'; +import { getVisibilityTitle } from '../../utils'; +import messages from '../../messages'; + +const UnitVisibilityInfo = ({ openVisibleModal, visibleToStaffOnly }) => { + const intl = useIntl(); + const { blockId } = useParams(); + const dispatch = useDispatch(); + const { + published, + hasChanges, + staffLockFrom, + releasedToStudents, + hasExplicitStaffLock, + } = useSelector(getCourseUnitData); + + const handleCourseUnitVisibility = () => { + dispatch(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, true)); + }; + + return ( + <> + + {getVisibilityTitle(intl, releasedToStudents, published, hasChanges)} + + {visibleToStaffOnly ? ( + <> +
+ {intl.formatMessage(messages.visibilityStaffOnlyTitle)} +
+ {!hasExplicitStaffLock && ( + + {intl.formatMessage(messages.visibilityHasExplicitStaffLockText, { sectionName: staffLockFrom })} + + )} + + ) : ( +
+ {intl.formatMessage(messages.visibilityStaffAndLearnersTitle)} +
+ )} + + {intl.formatMessage(messages.visibilityCheckboxTitle)} + + + ); +}; + +UnitVisibilityInfo.propTypes = { + openVisibleModal: PropTypes.func.isRequired, + visibleToStaffOnly: PropTypes.bool.isRequired, +}; + +export default UnitVisibilityInfo; diff --git a/src/course-unit/sidebar/components/sidebar-footer/index.jsx b/src/course-unit/sidebar/components/sidebar-footer/index.jsx new file mode 100644 index 0000000000..ee1e816bad --- /dev/null +++ b/src/course-unit/sidebar/components/sidebar-footer/index.jsx @@ -0,0 +1,57 @@ +import PropTypes from 'prop-types'; +import { Card, Stack } from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import messages from '../../messages'; +import UnitVisibilityInfo from './UnitVisibilityInfo'; +import ActionButtons from './ActionButtons'; + +const SidebarFooter = ({ + locationId, + openVisibleModal, + handlePublishing, + openDiscardModal, + visibleToStaffOnly, + displayUnitLocation, +}) => { + const intl = useIntl(); + + return ( + + + {displayUnitLocation ? ( + + {intl.formatMessage(messages.unitLocationDescription, { id: locationId })} + + ) : ( + <> + + + + )} + + + ); +}; + +SidebarFooter.propTypes = { + locationId: PropTypes.string, + displayUnitLocation: PropTypes.bool, + openDiscardModal: PropTypes.func.isRequired, + openVisibleModal: PropTypes.func.isRequired, + handlePublishing: PropTypes.func.isRequired, + visibleToStaffOnly: PropTypes.bool.isRequired, +}; + +SidebarFooter.defaultProps = { + displayUnitLocation: false, + locationId: null, +}; + +export default SidebarFooter; diff --git a/src/course-unit/sidebar/hooks.jsx b/src/course-unit/sidebar/hooks.jsx new file mode 100644 index 0000000000..bbe6c073fc --- /dev/null +++ b/src/course-unit/sidebar/hooks.jsx @@ -0,0 +1,43 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { getUnitReleaseStatus, UNIT_VISIBILITY_STATES } from '../constants'; +import messages from './messages'; +import { extractCourseUnitId } from './utils'; + +const useCourseUnitData = ({ + hasChanges, published, visibilityState, id, +}) => { + const intl = useIntl(); + const releaseStatus = getUnitReleaseStatus(intl); + const locationId = extractCourseUnitId(id); + const visibleToStaffOnly = visibilityState === UNIT_VISIBILITY_STATES.staffOnly; + const titleMessages = { + [UNIT_VISIBILITY_STATES.staffOnly]: messages.sidebarTitleVisibleToStaffOnly, + [UNIT_VISIBILITY_STATES.live]: messages.sidebarTitlePublishedAndLive, + // eslint-disable-next-line no-nested-ternary + default: published + ? (hasChanges ? messages.sidebarTitleDraftUnpublishedChanges + : messages.sidebarTitlePublishedNotYetReleased) + : messages.sidebarTitleDraftNeverPublished, + }; + + const releaseLabels = { + [UNIT_VISIBILITY_STATES.staffOnly]: releaseStatus.release, + [UNIT_VISIBILITY_STATES.live]: releaseStatus.released, + [UNIT_VISIBILITY_STATES.ready]: releaseStatus.scheduled, + default: releaseStatus.release, + }; + + const title = intl.formatMessage(titleMessages[visibilityState] || titleMessages.default); + const releaseLabel = releaseLabels[visibilityState] || releaseLabels.default; + + return { + title, + locationId, + releaseLabel, + visibilityState, + visibleToStaffOnly, + }; +}; + +export default useCourseUnitData; diff --git a/src/course-unit/sidebar/index.jsx b/src/course-unit/sidebar/index.jsx new file mode 100644 index 0000000000..f2817639b2 --- /dev/null +++ b/src/course-unit/sidebar/index.jsx @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import classNames from 'classnames'; +import { Card, useToggle } from '@openedx/paragon'; +import { InfoOutline as InfoOutlineIcon } from '@openedx/paragon/icons'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import ModalNotification from '../../generic/modal-notification'; +import { editCourseUnitVisibilityAndData } from '../data/thunk'; +import { getCourseUnitData } from '../data/selectors'; +import { PUBLISH_TYPES } from '../constants'; +import { SidebarBody, SidebarFooter, SidebarHeader } from './components'; +import useCourseUnitData from './hooks'; +import messages from './messages'; + +const Sidebar = ({ blockId, displayUnitLocation, ...props }) => { + const { + title, + locationId, + releaseLabel, + visibilityState, + visibleToStaffOnly, + } = useCourseUnitData(useSelector(getCourseUnitData)); + const intl = useIntl(); + const dispatch = useDispatch(); + const [isDiscardModalOpen, openDiscardModal, closeDiscardModal] = useToggle(false); + const [isVisibleModalOpen, openVisibleModal, closeVisibleModal] = useToggle(false); + + const handleCourseUnitVisibility = () => { + closeVisibleModal(); + dispatch(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, null)); + }; + + const handleCourseUnitDiscardChanges = () => { + closeDiscardModal(); + dispatch(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.discardChanges)); + }; + + const handleCourseUnitPublish = () => { + dispatch(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic)); + }; + + return ( + + + + + + + + ); +}; + +Sidebar.propTypes = { + blockId: PropTypes.string, + displayUnitLocation: PropTypes.bool, +}; + +Sidebar.defaultProps = { + blockId: null, + displayUnitLocation: false, +}; + +export default Sidebar; diff --git a/src/course-unit/sidebar/messages.js b/src/course-unit/sidebar/messages.js new file mode 100644 index 0000000000..7d9d161d5c --- /dev/null +++ b/src/course-unit/sidebar/messages.js @@ -0,0 +1,142 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + sidebarTitleDraftNeverPublished: { + id: 'course-authoring.course-unit.sidebar.title.draft.never-published', + defaultMessage: 'Draft (never published)', + }, + sidebarTitleVisibleToStaffOnly: { + id: 'course-authoring.course-unit.sidebar.title.visible.to-staff-only', + defaultMessage: 'Visible to staff only', + }, + sidebarTitlePublishedAndLive: { + id: 'course-authoring.course-unit.sidebar.title.published.live', + defaultMessage: 'Published and live', + }, + sidebarTitleDraftUnpublishedChanges: { + id: 'course-authoring.course-unit.sidebar.title.draft.unpublished', + defaultMessage: 'Draft (unpublished changes)', + }, + sidebarTitlePublishedNotYetReleased: { + id: 'course-authoring.course-unit.sidebar.title.published.not-yet-released', + defaultMessage: 'Published (not yet released)', + }, + sidebarHeaderUnitLocationTitle: { + id: 'course-authoring.course-unit.sidebar.header.unit-location.title', + defaultMessage: 'Unit location', + }, + sidebarBodyNote: { + id: 'course-authoring.course-unit.sidebar.body.note', + defaultMessage: 'Note: Do not hide graded assignments after they have been released.', + }, + publishInfoPreviouslyPublished: { + id: 'course-authoring.course-unit.publish.info.previously-published', + defaultMessage: 'Previously published', + }, + publishInfoDraftSaved: { + id: 'course-authoring.course-unit.publish.info.draft.saved', + defaultMessage: 'Draft saved on {editedOn} by {editedBy}', + }, + publishLastPublished: { + id: 'course-authoring.course-unit.publish.info.last.published', + defaultMessage: 'Last published {publishedOn} by {publishedBy}', + }, + releaseInfoUnscheduled: { + id: 'course-authoring.course-unit.release.info.unscheduled', + defaultMessage: 'Unscheduled', + }, + releaseInfoWithSection: { + id: 'course-authoring.course-unit.release.info.with-unit', + defaultMessage: 'with {sectionName}', + }, + visibilityIsVisibleToTitle: { + id: 'course-authoring.course-unit.visibility.is-visible-to.title', + defaultMessage: 'IS VISIBLE TO', + }, + visibilityWillBeVisibleToTitle: { + id: 'course-authoring.course-unit.visibility.will-be-visible-to.title', + defaultMessage: 'WILL BE VISIBLE TO', + }, + unitLocationTitle: { + id: 'course-authoring.course-unit.unit-location.title', + defaultMessage: 'LOCATION ID', + }, + unitLocationDescription: { + id: 'course-authoring.course-unit.unit-location.description', + defaultMessage: 'To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value', + }, + visibilityCheckboxTitle: { + id: 'course-authoring.course-unit.visibility.checkbox.title', + defaultMessage: 'Hide from learners', + }, + visibilityStaffOnlyTitle: { + id: 'course-authoring.course-unit.visibility.staff-only.title', + defaultMessage: 'Staff only', + }, + visibilityStaffAndLearnersTitle: { + id: 'course-authoring.course-unit.visibility.staff-and-learners.title', + defaultMessage: 'Staff and learners', + }, + visibilityHasExplicitStaffLockText: { + id: 'course-authoring.course-unit.visibility.has-explicit-staff-lock.text', + defaultMessage: 'with {sectionName}', + }, + actionButtonPublishTitle: { + id: 'course-authoring.course-unit.action-buttons.publish.title', + defaultMessage: 'Publish', + }, + actionButtonDiscardChangesTitle: { + id: 'course-authoring.course-unit.action-button.discard-changes.title', + defaultMessage: 'Discard changes', + }, + actionButtonCopyUnitTitle: { + id: 'course-authoring.course-unit.action-button.copy-unit.title', + defaultMessage: 'Copy unit', + }, + releaseStatusTitle: { + id: 'course-authoring.course-unit.status.release.title', + defaultMessage: 'RELEASE', + }, + releasedStatusTitle: { + id: 'course-authoring.course-unit.status.released.title', + defaultMessage: 'RELEASED', + }, + scheduledStatusTitle: { + id: 'course-authoring.course-unit.status.scheduled.title', + defaultMessage: 'SCHEDULED', + }, + modalDiscardUnitChangesTitle: { + id: 'course-authoring.course-unit.modal.discard-unit-changes.title', + defaultMessage: 'Discard changes', + }, + modalDiscardUnitChangesActionButtonText: { + id: 'course-authoring.course-unit.modal.discard-unit-changes.btn.action.text', + defaultMessage: 'Discard changes', + }, + modalDiscardUnitChangesCancelButtonText: { + id: 'course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text', + defaultMessage: 'Cancel', + }, + modalDiscardUnitChangesDescription: { + id: 'course-authoring.course-unit.modal.discard-unit-changes.description', + defaultMessage: 'Are you sure you want to revert to the last published version of the unit? You cannot undo this action.', + }, + modalMakeVisibilityTitle: { + id: 'course-authoring.course-unit.modal.make-visibility.title', + defaultMessage: 'Make visible to students', + }, + modalMakeVisibilityActionButtonText: { + id: 'course-authoring.course-unit.modal.make-visibility.btn.action.text', + defaultMessage: 'Make visible to students', + }, + modalMakeVisibilityCancelButtonText: { + id: 'course-authoring.course-unit.modal.make-visibility.btn.cancel.text', + defaultMessage: 'Cancel', + }, + modalMakeVisibilityDescription: { + id: 'course-authoring.course-unit.modal.make-visibility.description', + defaultMessage: 'If the unit was previously published and released to students, any changes you made to the unit when it was hidden will now be visible to students. Do you want to proceed?', + }, +}); + +export default messages; diff --git a/src/course-unit/sidebar/utils.js b/src/course-unit/sidebar/utils.js new file mode 100644 index 0000000000..af3263861f --- /dev/null +++ b/src/course-unit/sidebar/utils.js @@ -0,0 +1,102 @@ +import { + CheckCircle as CheckCircleIcon, + CheckCircleOutline as CheckCircleOutlineIcon, + InfoOutline as InfoOutlineIcon, +} from '@openedx/paragon/icons'; + +import { ICON_COLOR_VARIANTS, UNIT_VISIBILITY_STATES } from '../constants'; +import messages from './messages'; + +/** + * Get information about the publishing status. + * @param {Object} intl - The internationalization object. + * @param {boolean} hasChanges - Indicates if there are unpublished changes. + * @param {string} editedBy - The user who edited the content. + * @param {string} editedOn - The timestamp when the content was edited. + * @param {string} publishedBy - The user who last published the content. + * @param {string} publishedOn - The timestamp when the content was last published. + * @returns {string} Publish information based on the provided parameters. + */ +export const getPublishInfo = (intl, hasChanges, editedBy, editedOn, publishedBy, publishedOn) => { + let publishInfoText; + if (hasChanges && editedOn && editedBy) { + publishInfoText = intl.formatMessage(messages.publishInfoDraftSaved, { editedOn, editedBy }); + } else if (publishedOn && publishedBy) { + publishInfoText = intl.formatMessage(messages.publishLastPublished, { publishedOn, publishedBy }); + } else { + publishInfoText = intl.formatMessage(messages.publishInfoPreviouslyPublished); + } + + return publishInfoText; +}; + +/** + * Get information about the release status. + * @param {Object} intl - The internationalization object. + * @param {string} releaseDate - The release date of the content. + * @param {string} releaseDateFrom - The section name associated with the release date. + * @returns {string|ReactElement} Release information based on the provided parameters. + */ +export const getReleaseInfo = (intl, releaseDate, releaseDateFrom) => { + if (releaseDate) { + return { + isScheduled: true, + releaseDate, + releaseDateFrom, + sectionNameMessage: intl.formatMessage(messages.releaseInfoWithSection, { sectionName: releaseDateFrom }), + }; + } + return { + isScheduled: false, + message: intl.formatMessage(messages.releaseInfoUnscheduled), + }; +}; + +/** + * Get the visibility title. + * @param {Object} intl - The internationalization object. + * @param {boolean} releasedToStudents - Indicates if the content is released to students. + * @param {boolean} published - Indicates if the content is published. + * @param {boolean} hasChanges - Indicates if there are unpublished changes. + * @returns {string} The visibility title determined by the provided parameters. + */ +export const getVisibilityTitle = (intl, releasedToStudents, published, hasChanges) => { + if (releasedToStudents && published && !hasChanges) { + return intl.formatMessage(messages.visibilityIsVisibleToTitle); + } + + return intl.formatMessage(messages.visibilityWillBeVisibleToTitle); +}; + +/** + * Get the icon variant based on the provided visibility state and publication status. + * @param {string} visibilityState - The visibility state of the content. + * @param {boolean} published - Indicates if the content is published. + * @param {boolean} hasChanges - Indicates if there are unpublished changes. + * @returns {Object} An object containing the icon component and color variant. + * - iconSrc: The source component for the icon. + * - colorVariant: The color variant for the icon. + */ +export const getIconVariant = (visibilityState, published, hasChanges) => { + const iconVariants = { + [UNIT_VISIBILITY_STATES.staffOnly]: { iconSrc: InfoOutlineIcon, colorVariant: ICON_COLOR_VARIANTS.BLACK }, + [UNIT_VISIBILITY_STATES.live]: { iconSrc: CheckCircleIcon, colorVariant: ICON_COLOR_VARIANTS.GREEN }, + publishedNoChanges: { iconSrc: CheckCircleOutlineIcon, colorVariant: ICON_COLOR_VARIANTS.BLACK }, + publishedWithChanges: { iconSrc: InfoOutlineIcon, colorVariant: ICON_COLOR_VARIANTS.BLACK }, + default: { iconSrc: InfoOutlineIcon, colorVariant: ICON_COLOR_VARIANTS.BLACK }, + }; + if (visibilityState in iconVariants) { + return iconVariants[visibilityState]; + } + if (published) { + return hasChanges ? iconVariants.publishedWithChanges : iconVariants.publishedNoChanges; + } + return iconVariants.default; +}; + +/** + * Extracts the clear course unit ID from the given course unit data. + * @param {string} id - The course unit ID. + * @returns {string} The clear course unit ID extracted from the provided data. + */ +export const extractCourseUnitId = (id) => id.match(/block@(.+)$/)[1]; diff --git a/src/export-page/export-modal-error/ExportModalError.jsx b/src/export-page/export-modal-error/ExportModalError.jsx index 56921a84c4..64d67f2f16 100644 --- a/src/export-page/export-modal-error/ExportModalError.jsx +++ b/src/export-page/export-modal-error/ExportModalError.jsx @@ -3,8 +3,9 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { useDispatch, useSelector } from 'react-redux'; import { getConfig } from '@edx/frontend-platform'; import PropTypes from 'prop-types'; +import { Error as ErrorIcon } from '@openedx/paragon/icons'; -import ModalError from '../../generic/modal-error/ModalError'; +import ModalNotification from '../../generic/modal-notification'; import { getError, getIsErrorModalOpen } from '../data/selectors'; import { updateIsErrorModalOpen } from '../data/slice'; import messages from './messages'; @@ -20,7 +21,7 @@ const ExportModalError = ({ const handleUnitRedirect = () => { window.location.assign(unitErrorUrl); }; const handleRedirectCourseHome = () => { window.location.assign(`${getConfig().STUDIO_BASE_URL}/course/${courseId}`); }; return ( - dispatch(updateIsErrorModalOpen(false))} handleAction={unitErrorUrl ? handleUnitRedirect : handleRedirectCourseHome} + variant="danger" + icon={ErrorIcon} /> ); }; diff --git a/src/course-outline/delete-modal/DeleteModal.jsx b/src/generic/delete-modal/DeleteModal.jsx similarity index 76% rename from src/course-outline/delete-modal/DeleteModal.jsx rename to src/generic/delete-modal/DeleteModal.jsx index eff6cb3176..1f9ebc286f 100644 --- a/src/course-outline/delete-modal/DeleteModal.jsx +++ b/src/generic/delete-modal/DeleteModal.jsx @@ -1,21 +1,17 @@ -import React from 'react'; import PropTypes from 'prop-types'; import { ActionRow, Button, AlertModal, } from '@openedx/paragon'; -import { useSelector } from 'react-redux'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { COURSE_BLOCK_NAMES } from '../constants'; -import { getCurrentItem } from '../data/selectors'; import messages from './messages'; -const DeleteModal = ({ isOpen, close, onDeleteSubmit }) => { +const DeleteModal = ({ + category, isOpen, close, onDeleteSubmit, +}) => { const intl = useIntl(); - let { category } = useSelector(getCurrentItem); - category = COURSE_BLOCK_NAMES[category]?.name.toLowerCase(); return ( { DeleteModal.propTypes = { isOpen: PropTypes.bool.isRequired, close: PropTypes.func.isRequired, + category: PropTypes.string.isRequired, onDeleteSubmit: PropTypes.func.isRequired, }; diff --git a/src/course-outline/delete-modal/DeleteModal.test.jsx b/src/generic/delete-modal/DeleteModal.test.jsx similarity index 94% rename from src/course-outline/delete-modal/DeleteModal.test.jsx rename to src/generic/delete-modal/DeleteModal.test.jsx index e03e4a4b8b..5edf101f81 100644 --- a/src/course-outline/delete-modal/DeleteModal.test.jsx +++ b/src/generic/delete-modal/DeleteModal.test.jsx @@ -1,7 +1,6 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { useSelector } from 'react-redux'; import { initializeMockApp } from '@edx/frontend-platform'; import MockAdapter from 'axios-mock-adapter'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; @@ -30,16 +29,13 @@ jest.mock('@edx/frontend-platform/i18n', () => ({ }), })); -const currentItemMock = { - displayName: 'Delete', -}; - const renderComponent = (props) => render( @@ -60,7 +56,6 @@ describe('', () => { store = initializeStore(); axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - useSelector.mockReturnValue(currentItemMock); }); it('render DeleteModal component correctly', () => { diff --git a/src/course-outline/delete-modal/messages.js b/src/generic/delete-modal/messages.js similarity index 100% rename from src/course-outline/delete-modal/messages.js rename to src/generic/delete-modal/messages.js diff --git a/src/generic/modal-error/ModalError.jsx b/src/generic/modal-notification/index.jsx similarity index 72% rename from src/generic/modal-error/ModalError.jsx rename to src/generic/modal-notification/index.jsx index a4de402b69..ae1dc2ba87 100644 --- a/src/generic/modal-error/ModalError.jsx +++ b/src/generic/modal-notification/index.jsx @@ -1,16 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import { ActionRow, AlertModal, Button } from '@openedx/paragon'; -import { Error } from '@openedx/paragon/icons'; -const ModalError = ({ - isOpen, title, message, handleCancel, handleAction, cancelButtonText, actionButtonText, +const ModalNotification = ({ + isOpen, title, message, handleCancel, handleAction, cancelButtonText, actionButtonText, variant, icon, }) => ( @@ -22,7 +21,7 @@ const ModalError = ({ ); -ModalError.propTypes = { +ModalNotification.propTypes = { isOpen: PropTypes.bool.isRequired, title: PropTypes.string.isRequired, message: PropTypes.string.isRequired, @@ -30,6 +29,13 @@ ModalError.propTypes = { handleAction: PropTypes.func.isRequired, cancelButtonText: PropTypes.string.isRequired, actionButtonText: PropTypes.string.isRequired, + variant: PropTypes.string, + icon: PropTypes.elementType, }; -export default ModalError; +ModalNotification.defaultProps = { + variant: 'default', + icon: undefined, +}; + +export default ModalNotification; diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index b7f55cd56d..5ddf05c815 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -1010,8 +1010,71 @@ "course-authoring.certificates.sidebar.about2.description-2": "{strongText} delete certificates after a course has started; learners who have already earned certificates will no longer be able to access them.", "course-authoring.certificates.sidebar.about2.description-2.strong": "Do not", "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", - "course-authoring.course-unit.add.component.button.text": "Add Component:", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience.", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions" } diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index e3311ad17a..ad312d6647 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -1013,5 +1013,69 @@ "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience." } diff --git a/src/i18n/messages/de_DE.json b/src/i18n/messages/de_DE.json index 5b567106ab..ad043c3f7c 100644 --- a/src/i18n/messages/de_DE.json +++ b/src/i18n/messages/de_DE.json @@ -1013,5 +1013,69 @@ "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience.", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?" } diff --git a/src/i18n/messages/es_419.json b/src/i18n/messages/es_419.json index 29a33d6318..8ecd721182 100644 --- a/src/i18n/messages/es_419.json +++ b/src/i18n/messages/es_419.json @@ -1013,5 +1013,69 @@ "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience." } diff --git a/src/i18n/messages/fa_IR.json b/src/i18n/messages/fa_IR.json index 423f64688a..a9d33d6636 100644 --- a/src/i18n/messages/fa_IR.json +++ b/src/i18n/messages/fa_IR.json @@ -36,5 +36,69 @@ "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience." } diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 8a81812490..6287473708 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -1013,5 +1013,69 @@ "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience." } diff --git a/src/i18n/messages/fr_CA.json b/src/i18n/messages/fr_CA.json index dc8f5d57fa..071a2b2934 100644 --- a/src/i18n/messages/fr_CA.json +++ b/src/i18n/messages/fr_CA.json @@ -1013,5 +1013,69 @@ "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience." } diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index e3311ad17a..ad312d6647 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -1013,5 +1013,69 @@ "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience." } diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index e3311ad17a..ad312d6647 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -1013,5 +1013,69 @@ "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience." } diff --git a/src/i18n/messages/it_IT.json b/src/i18n/messages/it_IT.json index a17d786ee6..b743fe0365 100644 --- a/src/i18n/messages/it_IT.json +++ b/src/i18n/messages/it_IT.json @@ -1013,5 +1013,69 @@ "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience." } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index e3311ad17a..ad312d6647 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -1013,5 +1013,69 @@ "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience." } diff --git a/src/i18n/messages/pt_PT.json b/src/i18n/messages/pt_PT.json index b345c4711f..b0567f5942 100644 --- a/src/i18n/messages/pt_PT.json +++ b/src/i18n/messages/pt_PT.json @@ -1013,5 +1013,69 @@ "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience." } diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index e3311ad17a..ad312d6647 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -1013,5 +1013,69 @@ "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience." } diff --git a/src/i18n/messages/uk.json b/src/i18n/messages/uk.json index e3311ad17a..ad312d6647 100644 --- a/src/i18n/messages/uk.json +++ b/src/i18n/messages/uk.json @@ -1013,5 +1013,69 @@ "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience." } diff --git a/src/i18n/messages/zh_CN.json b/src/i18n/messages/zh_CN.json index e3311ad17a..ad312d6647 100644 --- a/src/i18n/messages/zh_CN.json +++ b/src/i18n/messages/zh_CN.json @@ -1013,5 +1013,69 @@ "course-authoring.certificates.sidebar.learnmore.button": "Learn more about certificates", "course-authoring.course-unit.modal.button.text": "Select", "course-authoring.course-unit.modal.container.title": "Add {componentTitle} component", - "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel" + "course-authoring.course-unit.modal.container.cancel.button.text": "Cancel", + "course-authoring.course-unit.sidebar.title.draft.never-published": "Draft (never published)", + "course-authoring.course-unit.sidebar.title.visible.to-staff-only": "Visible to staff only", + "course-authoring.course-unit.sidebar.title.published.live": "Published and live", + "course-authoring.course-unit.sidebar.title.draft.unpublished": "Draft (unpublished changes)", + "course-authoring.course-unit.sidebar.title.published.not-yet-released": "Published (not yet released)", + "course-authoring.course-unit.sidebar.header.unit-location.title": "Unit location", + "course-authoring.course-unit.sidebar.body.note": "Note: Do not hide graded assignments after they have been released.", + "course-authoring.course-unit.publish.info.previously-published": "Previously published", + "course-authoring.course-unit.publish.info.draft.saved": "Draft saved on {editedOn} by {editedBy}", + "course-authoring.course-unit.publish.info.last.published": "Last published {publishedOn} by {publishedBy}", + "course-authoring.course-unit.release.info.unscheduled": "Unscheduled", + "course-authoring.course-unit.release.info.with-unit": "with {sectionName}", + "course-authoring.course-unit.visibility.is-visible-to.title": "IS VISIBLE TO", + "course-authoring.course-unit.visibility.will-be-visible-to.title": "WILL BE VISIBLE TO", + "course-authoring.course-unit.unit-location.title": "LOCATION ID", + "course-authoring.course-unit.unit-location.description": "To create a link to this unit from an HTML component in this course, enter /jump_to_id/{id} as the URL value", + "course-authoring.course-unit.visibility.checkbox.title": "Hide from learners", + "course-authoring.course-unit.visibility.staff-only.title": "Staff only", + "course-authoring.course-unit.visibility.staff-and-learners.title": "Staff and learners", + "course-authoring.course-unit.visibility.has-explicit-staff-lock.text": "with {sectionName}", + "course-authoring.course-unit.action-buttons.publish.title": "Publish", + "course-authoring.course-unit.action-button.discard-changes.title": "Discard changes", + "course-authoring.course-unit.action-button.copy-unit.title": "Copy unit", + "course-authoring.course-unit.status.release.title": "RELEASE", + "course-authoring.course-unit.status.released.title": "RELEASED", + "course-authoring.course-unit.status.scheduled.title": "SCHEDULED", + "course-authoring.course-unit.xblock.button.edit.alt": "Edit Item", + "course-authoring.course-unit.xblock.button.copy.label": "Copy", + "course-authoring.course-unit.xblock.button.duplicate.label": "Duplicate", + "course-authoring.course-unit.xblock.button.move.label": "Move", + "course-authoring.course-unit.xblock.button.manageAccess.label": "Manage access", + "course-authoring.course-unit.xblock.button.delete.label": "Delete", + "course-authoring.course-unit.xblock.button.actions.alt": "Actions", + "course-authoring.course-unit.modal.discard-unit-changes.title": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.action.text": "Discard changes", + "course-authoring.course-unit.modal.discard-unit-changes.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.discard-unit-changes.description": "Are you sure you want to revert to the last published version of the unit? You cannot undo this action.", + "course-authoring.course-unit.modal.make-visibility.title": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.action.text": "Make visible to students", + "course-authoring.course-unit.modal.make-visibility.btn.cancel.text": "Cancel", + "course-authoring.course-unit.modal.make-visibility.description": "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?", + "course-authoring.group-configurations.heading-title": "Group configurations", + "course-authoring.group-configurations.heading-sub-title": "Settings", + "course-authoring.group-configurations.container.empty-content-groups": "In the {outlineComponentLink}, use this group to control access to a component.", + "course-authoring.group-configurations.container.empty-experiment-group": "This group configuration is not in use. Start by adding a content experiment to any Unit via the {outlineComponentLink}.", + "course-authoring.group-configurations.container.course-outline": "Course outline", + "course-authoring.group-configurations.container.action.edit": "Edit", + "course-authoring.group-configurations.container.action.delete": "Delete", + "course-authoring.group-configurations.container.access-to": "This group controls access to:", + "course-authoring.group-configurations.container.experiment-access-to": "This group configuration is used in:", + "course-authoring.group-configurations.container.contains-groups": "Contains {len} groups", + "course-authoring.group-configurations.container.contains-group": "Contains 1 group", + "course-authoring.group-configurations.container.not-in-use": "Not in use", + "course-authoring.group-configurations.container.used-in-locations": "Used in {len} locations", + "course-authoring.group-configurations.container.used-in-location": "Used in 1 location", + "course-authoring.group-configurations.container.title-id": "ID: {id}", + "course-authoring.group-configurations.experiment-group.title": "Experiment group configurations", + "course-authoring.group-configurations.experiment-group.add-new-group": "New group configuration", + "course-authoring.group-configurations.empty-placeholder.title": "You have not created any content groups yet.", + "course-authoring.group-configurations.experimental-empty-placeholder.title": "You have not created any group configurations yet.", + "course-authoring.group-configurations.empty-placeholder.button": "Add your first content group", + "course-authoring.group-configurations.experimental-empty-placeholder.button": "Add your first group configuration", + "course-authoring.group-configurations.content-groups.add-new-group": "New content group", + "course-authoring.course-unit.general.alert.unpublished-version.description": "Note: The last published version of this unit is live. By publishing changes you will change the student experience." }