Skip to content

Commit

Permalink
feat: [AXIMST-469] Unit page - display unit and xblock visibility mes…
Browse files Browse the repository at this point in the history
…sages (#162)

* feat: [AXIMST-469] Unit page - display unit and xblock visibility messages

* fix: [AXIMST-469] after review
  • Loading branch information
ihor-romaniuk authored and monteri committed Apr 29, 2024
1 parent 11198c3 commit 7cc8899
Show file tree
Hide file tree
Showing 25 changed files with 1,346 additions and 36 deletions.
3 changes: 2 additions & 1 deletion src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,14 @@ const CourseUnit = ({ courseId }) => {
updateOrder={finalizeXBlockOrder}
>
{unitXBlocks.map(({
name, id, blockType: type, shouldScroll, userPartitionInfo,
name, id, blockType: type, shouldScroll, userPartitionInfo, validationMessages,
}) => (
<CourseXBlock
id={id}
key={id}
title={name}
type={type}
validationMessages={validationMessages}
shouldScroll={shouldScroll}
unitXBlockActions={unitXBlockActions}
handleConfigureSubmit={handleConfigureSubmit}
Expand Down
6 changes: 4 additions & 2 deletions src/course-unit/course-xblock/CourseXBlock.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import { COURSE_BLOCK_NAMES } from '../../constants';
import { copyToClipboard } from '../../generic/data/thunks';
import { COMPONENT_TYPES } from '../constants';
import XBlockMessages from './xblock-messages/XBlockMessages';
import ContentIFrame from './ContentIFrame';
import XBlockContent from './xblock-content/XBlockContent';
import XBlockMessages from './xblock-messages/XBlockMessages';
import { getIFrameUrl } from './urls';
import messages from './messages';

Expand Down Expand Up @@ -131,7 +132,8 @@ const CourseXBlock = ({
)}
/>
<Card.Section>
<ContentIFrame id={id} title={title} elementId={id} iframeUrl={iframeUrl} />
<XBlockMessages validationMessages={validationMessages} />
<XBlockContent id={id} title={title} elementId={id} iframeUrl={iframeUrl} />
</Card.Section>
</Card>
</div>
Expand Down
5 changes: 5 additions & 0 deletions src/course-unit/course-xblock/CourseXBlock.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
font-size: $font-size-sm;
}

.pgn__card-header-subtitle-md {
margin-top: 0;
font-size: 1rem;
}

.pgn__card-header-title-md {
font: 700 1.375rem/1.75rem $font-family-sans-serif;
color: $black;
Expand Down
17 changes: 17 additions & 0 deletions src/course-unit/course-xblock/CourseXBlock.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -310,4 +310,21 @@ describe('<CourseXBlock />', () => {
expect(getByText(visibilityMessage)).toBeInTheDocument();
});
});

it('displays a visibility message if item has accessible restrictions', async () => {
const { getByText } = renderComponent(
{
userPartitionInfo: {
...userPartitionInfoFormatted,
selectedGroupsLabel: 'Visibility group 1',
},
},
);

await waitFor(() => {
const visibilityMessage = messages.visibilityMessage.defaultMessage
.replace('{selectedGroupsLabel}', 'Visibility group 1');
expect(getByText(visibilityMessage)).toBeInTheDocument();
});
});
});
5 changes: 5 additions & 0 deletions src/course-unit/course-xblock/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ export const MESSAGE_TYPES = {
export const IFRAME_FEATURE_POLICY = (
'microphone *; camera *; midi *; geolocation *; encrypted-media *, clipboard-write *'
);

export const MESSAGE_ERROR_TYPES = {
error: 'error',
warning: 'warning',
};
8 changes: 8 additions & 0 deletions src/course-unit/course-xblock/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ const messages = defineMessages({
id: 'course-authoring.course-unit.xblock.iframe.error.text',
defaultMessage: 'Unit iframe failed to load. Server possibly returned 4xx or 5xx response.',
},
visibilityMessage: {
id: 'course-authoring.course-unit.xblock.visibility.message',
defaultMessage: 'Access restricted to: {selectedGroupsLabel}',
},
validationSummary: {
id: 'course-authoring.course-unit.xblock.validation.summary',
defaultMessage: 'This component has validation issues.',
},
});

export default messages;
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';

import { LoadingSpinner } from '../../generic/Loading';
import AlertMessage from '../../generic/alert-message';
import { IFRAME_FEATURE_POLICY } from './constants';
import { useIFrameBehavior } from './hooks';
import messages from './messages';
import { LoadingSpinner } from '../../../generic/Loading';
import AlertMessage from '../../../generic/alert-message';
import { IFRAME_FEATURE_POLICY } from '../constants';
import { useIFrameBehavior } from '../hooks';
import messages from '../messages';

const ContentIFrame = ({
const XBlockContent = ({
id,
iframeUrl,
elementId,
Expand All @@ -24,7 +24,7 @@ const ContentIFrame = ({
elementId, id, iframeUrl, onLoaded,
});

const contentIFrameProps = {
const iframeProps = {
id: elementId,
src: iframeUrl,
allow: IFRAME_FEATURE_POLICY,
Expand All @@ -48,21 +48,21 @@ const ContentIFrame = ({
</div>
)
)}
<iframe title={title} className="w-100" {...contentIFrameProps} />
<iframe title={title} className="w-100" {...iframeProps} />
</div>
);
};

ContentIFrame.propTypes = {
XBlockContent.propTypes = {
iframeUrl: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
elementId: PropTypes.string.isRequired,
onLoaded: PropTypes.func,
title: PropTypes.node.isRequired,
};

ContentIFrame.defaultProps = {
XBlockContent.defaultProps = {
onLoaded: () => {},
};

export default ContentIFrame;
export default XBlockContent;
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { shallow } from '@edx/react-unit-test-utils';
import { render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';

import * as hooks from './hooks';
import ContentIFrame from './ContentIFrame';
import messages from './messages';
import * as hooks from '../hooks';
import messages from '../messages';
import XBlockContent from './XBlockContent';

jest.mock('./hooks', () => ({
jest.mock('../hooks', () => ({
useIFrameBehavior: jest.fn(),
}));

Expand All @@ -30,12 +30,12 @@ const props = {

const RootComponent = () => (
<IntlProvider locale="en">
<ContentIFrame {...props} />
<XBlockContent {...props} />
</IntlProvider>
);

let el;
describe('ContentIFrame Component', () => {
describe('<XBlockContent />', () => {
beforeEach(() => {
jest.clearAllMocks();
});
Expand Down
2 changes: 1 addition & 1 deletion src/course-unit/header-title/HeaderTitle.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const HeaderTitle = ({

return (
<>
<div className="d-flex align-items-center lead" data-testid="unit-header-title">
<div className="d-flex align-items-center lead">
{isTitleEditFormOpen ? (
<Form.Group className="m-0">
<Form.Control
Expand Down
8 changes: 8 additions & 0 deletions src/course-unit/header-title/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ const messages = defineMessages({
defaultMessage: 'Access to some content in this unit is restricted to specific groups of learners.',
description: 'The label text of some content restriction in this unit',
},
definedVisibilityMessage: {
id: 'course-authoring.course-unit.heading.visibility.defined.message',
defaultMessage: 'Access to this unit is restricted to: {selectedGroupsLabel}',
},
commonVisibilityMessage: {
id: 'course-authoring.course-unit.heading.visibility.common.message',
defaultMessage: 'Access to some content in this unit is restricted to specific groups of learners.',
},
});

export default messages;
87 changes: 86 additions & 1 deletion src/i18n/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -996,8 +996,10 @@
"course-authoring.certificates.heading.subtitle": "Settings",
"course-authoring.certificates.heading.action.button.preview": "Preview certificate",
"course-authoring.certificates.heading.action.button.deactivate": "Deactivate",
"course-authoring.certificates.heading.action.button.activate": "Activate",
"course-authoring.certificates.nocertificate.text": "You haven't added any certificates to this course yet.",
"course-authoring.certificates.setup.certificate.button": "Add your first certificate",
"course-authoring.certificates.setup.certificate.button.alt": "Add your first certificate",
"course-authoring.certificates.without.modes.text": "This course does not use a mode that offers certificates.",
"course-authoring.certificates.sidebar.about.title": "Working with certificates",
"course-authoring.certificates.sidebar.about.description-1": "Specify a course title to use on the certificate if the course's official title is too long to be displayed well.",
Expand All @@ -1011,6 +1013,48 @@
"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.certificates.card.create": "Create",
"course-authoring.certificates.card.cancel": "Cancel",
"course-authoring.certificates.details.section.title": "Certificate details",
"course-authoring.certificates.details.course.title": "Course title",
"course-authoring.certificates.details.course.title.override": "Course title override",
"course-authoring.certificates.details.course.title.override.description": "Specify an alternative to the official course title to display on certificates. Leave blank to use the official course title.",
"course-authoring.certificates.details.course.number": "Course number",
"course-authoring.certificates.details.course.number.override": "Course number override",
"course-authoring.certificates.signatories.title": "Signatory",
"course-authoring.certificates.signatories.recommendation": "It is strongly recommended that you include four or fewer signatories. If you include additional signatories, preview the certificate in Print View to ensure the certificate will print correctly on one page.",
"course-authoring.certificates.signatories.section.title": "Certificate signatories",
"course-authoring.certificates.signatories.add.signatory.button": "Add additional signatory",
"course-authoring.certificates.signatories.add.signatory.button.description": "(Add signatories for a certificate)",
"course-authoring.certificates.signatories.edit.tooltip": "Edit",
"course-authoring.certificates.signatories.delete.tooltip": "Delete",
"course-authoring.certificates.signatories.name.label": "Name:",
"course-authoring.certificates.signatories.name.placeholder": "Name of the signatory",
"course-authoring.certificates.signatories.name.description": "The name of this signatory as it should appear on certificates.",
"course-authoring.certificates.signatories.title.label": "Title:",
"course-authoring.certificates.signatories.title.placeholder": "Title of the signatory",
"course-authoring.certificates.signatories.title.description": "Titles more than 100 characters may prevent students from printing their certificate on a single page.",
"course-authoring.certificates.signatories.organization.label": "Organization:",
"course-authoring.certificates.signatories.organization.placeholder": "Organization of the signatory",
"course-authoring.certificates.signatories.organization.description": "The organization that this signatory belongs to, as it should appear on certificates.",
"course-authoring.certificates.signatories.image.label": "Signature image",
"course-authoring.certificates.signatories.image.placeholder": "Path to signature image",
"course-authoring.certificates.signatories.image.description": "Image must be in PNG format",
"course-authoring.certificates.signatories.upload.image.button": "Upload signature image",
"course-authoring.certificates.signatories.upload.modal": "Upload",
"course-authoring.certificates.signatories.confirm-modal": "Delete \"{name}\" from the list of signatories?",
"course-authoring.certificates.signatories.confirm-modal.message": "This action cannot be undone.",
"course-authoring.certificates.modal-dropzone.text": "Drag and drop your image here or click to upload",
"course-authoring.certificates.modal-dropzone.dropzone-alt": "Uploaded image for course certificate",
"course-authoring.certificates.modal-dropzone.validation.text": "Only {types} files can be uploaded. Please select a file ending in {extensions} to upload.",
"course-authoring.certificates.modal-dropzone.image.button": "Upload signature image",
"course-authoring.certificates.modal-dropzone.cancel.modal": "Cancel",
"course-authoring.certificates.modal-dropzone.upload.modal": "Upload",
"course-authoring.certificates.details.confirm-modal": "Delete this certificate?",
"course-authoring.certificates.details.confirm-modal.message": "Deleting this certificate is permanent and cannot be undone.",
"course-authoring.certificates.details.confirm.edit": "Edit this certificate?",
"course-authoring.certificates.details.confirm.edit.message": "This certificate has already been activated and is live. Are you sure you want to continue editing?",
"course-authoring.certificates.details.confirm.edit.btn.text": "Yes, continue",
"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",
Expand Down Expand Up @@ -1069,6 +1113,21 @@
"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.group-configurations.sidebar.about.title": "Content groups",
"course-authoring.group-configurations.sidebar.about.description-1": "If you have cohorts enabled in your course, you can use content groups to create cohort-specific courseware. In other words, you can customize the content that particular cohorts see in your course.",
"course-authoring.group-configurations.sidebar.about.description-2": "Each content group that you create can be associated with one or more cohorts. In addition to making course content available to all learners, you can restrict access to some content to learners in specific content groups. Only learners in the cohorts that are associated with the specified content groups see the additional content.",
"course-authoring.group-configurations.sidebar.about.description-3": "Click {strongText} to add a new content group. To edit the name of a content group, hover over its box and click {strongText2}. You can delete a content group only if it is not in use by a unit. To delete a content group, hover over its box and click the delete icon.Drag sections, subsections, and units to new locations in the outline.",
"course-authoring.group-configurations.sidebar.about.description-3.strong": "New content group",
"course-authoring.group-configurations.sidebar.about-2.title": "Experiment group configurations",
"course-authoring.group-configurations.sidebar.about-2.description-1": "Use experiment group configurations if you are conducting content experiments, also known as A/B testing, in your course. Experiment group configurations define how many groups of learners are in a content experiment. When you create a content experiment for a course, you select the group configuration to use.",
"course-authoring.group-configurations.sidebar.about-2.description-2": "Click {strongText} to add a new configuration. To edit a configuration, hover over its box and click {strongText2}. You can delete a group configuration only if it is not in use in an experiment. To delete a configuration, hover over its box and click the delete.",
"course-authoring.group-configurations.sidebar.about-2.description-2.strong": "New group configuration",
"course-authoring.group-configurations.sidebar.about-3.title": "Enrollment track groups",
"course-authoring.group-configurations.sidebar.about-3.description-1": "Enrollment track groups allow you to offer different course content to learners in each enrollment track. Learners enrolled in each enrollment track in your course are automatically included in the corresponding enrollment track group.",
"course-authoring.group-configurations.sidebar.about-3.description-2": "On unit pages in the course outline, you can restrict access to components to learners based on their enrollment track.",
"course-authoring.group-configurations.sidebar.about-3.description-3": "You cannot edit enrollment track groups, but you can expand each group to view details of the course content that is designated for learners in the group.",
"course-authoring.group-configurations.sidebar.about.description.strong-edit": "edit",
"course-authoring.group-configurations.sidebar.learnmore.button": "Learn more",
"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",
Expand Down Expand Up @@ -1122,5 +1181,31 @@
"course-authoring.textbooks.form.upload-modal.help-text": "File must be in PDF format",
"course-authoring.textbooks.form.delete-modal.title": "Delete “{textbookTitle}”?",
"course-authoring.textbooks.form.delete-modal.description": "Deleting a textbook cannot be undone and once deleted any reference to it in your courseware's navigation will also be removed.",
"course-authoring.course-unit.xblock.iframe.error.text": "Unit iframe failed to load. Server possibly returned 4xx or 5xx response."
"course-authoring.course-unit.xblock.iframe.error.text": "Unit iframe failed to load. Server possibly returned 4xx or 5xx response.",
"course-authoring.course-unit.paste-component.btn.text": "Paste component",
"course-authoring.course-unit.popover.content.text": "From:",
"course-authoring.course-unit.paste-component.whats-in-clipboard.text": "What's in my clipboard?",
"course-authoring.course-unit.paste-notification.has-conflicting-errors.title": "Files need to be updated manually.",
"course-authoring.course-unit.paste-notification.has-conflicting-errors.description": "The following files must be updated manually for components to work as intended:",
"course-authoring.course-unit.paste-notification.has-conflicting-errors.button.text": "Upload files",
"course-authoring.course-unit.paste-notification.has-errors.title": "Some errors occurred",
"course-authoring.course-unit.paste-notification.has-errors.description": "The following required files could not be added to the course:",
"course-authoring.course-unit.paste-notification.has-new-files.title": "New file(s) added to Files & Uploads.",
"course-authoring.course-unit.paste-notification.has-new-files.description": "The following required files were imported to this course:",
"course-authoring.course-unit.paste-notification.has-new-files.button.text": "View files",
"course-authoring.group-configurations.container.delete-modal.subtitle": "content group",
"course-authoring.group-configurations.container.delete-restriction": "Cannot delete when in use by a unit",
"course-authoring.group-configurations.content-groups.new-group.header": "Content group name *",
"course-authoring.group-configurations.content-groups.new-group.input.placeholder": "This is the name of the group",
"course-authoring.group-configurations.content-groups.new-group.invalid-message": "All groups must have a unique name.",
"course-authoring.group-configurations.content-groups.new-group.cancel": "Cancel",
"course-authoring.group-configurations.content-groups.edit-group.delete": "Delete",
"course-authoring.group-configurations.content-groups.new-group.create": "Create",
"course-authoring.group-configurations.content-groups.edit-group.save": "Save",
"course-authoring.group-configurations.content-groups.new-group.required-error": "Group name is required",
"course-authoring.group-configurations.content-groups.edit-group.alert-group-in-usage": "This content group is used in one or more units.",
"course-authoring.course-unit.xblock.visibility.message": "Access restricted to: {selectedGroupsLabel}",
"course-authoring.course-unit.xblock.validation.summary": "This component has validation issues.",
"course-authoring.course-unit.heading.visibility.defined.message": "Access to this unit is restricted to: {selectedGroupsLabel}",
"course-authoring.course-unit.heading.visibility.common.message": "Access to some content in this unit is restricted to specific groups of learners."
}
Loading

0 comments on commit 7cc8899

Please sign in to comment.