Skip to content

Commit

Permalink
feat: [AXIMST-23] Course unit - Sidebar with unit info (#117)
Browse files Browse the repository at this point in the history
* feat: added Sidebar with unit info

* feat: added unit location

* refactor: added legacy behavior

* feat: added live variant

* refactor: code refactoring

* feat: added tests and translations

* feat: added new font size

* refactor: after review
  • Loading branch information
PKulkoRaccoonGang committed Feb 13, 2024
1 parent 79d5a2d commit 89db038
Show file tree
Hide file tree
Showing 31 changed files with 1,469 additions and 24 deletions.
16 changes: 11 additions & 5 deletions src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { Container, Layout } from '@edx/paragon';
import { Container, Layout, Stack } from '@edx/paragon';
import { useIntl, injectIntl } from '@edx/frontend-platform/i18n';
import { ErrorAlert } from '@edx/frontend-lib-content-components';

Expand All @@ -17,6 +17,7 @@ import HeaderTitle from './header-title/HeaderTitle';
import Breadcrumbs from './breadcrumbs/Breadcrumbs';
import HeaderNavigations from './header-navigations/HeaderNavigations';
import Sequence from './course-sequence';
import Sidebar from './sidebar';
import { useCourseUnit } from './hooks';
import messages from './messages';

Expand Down Expand Up @@ -85,9 +86,9 @@ const CourseUnit = ({ courseId }) => {
handleCreateNewCourseXblock={handleCreateNewCourseXblock}
/>
<Layout
lg={[{ span: 9 }, { span: 3 }]}
md={[{ span: 9 }, { span: 3 }]}
sm={[{ span: 9 }, { span: 3 }]}
lg={[{ span: 8 }, { span: 4 }]}
md={[{ span: 8 }, { span: 4 }]}
sm={[{ span: 8 }, { span: 3 }]}
xs={[{ span: 9 }, { span: 3 }]}
xl={[{ span: 9 }, { span: 3 }]}
>
Expand All @@ -97,7 +98,12 @@ const CourseUnit = ({ courseId }) => {
handleCreateNewCourseXblock={handleCreateNewCourseXblock}
/>
</Layout.Element>
<Layout.Element />
<Layout.Element>
<Stack gap={3}>
<Sidebar />
<Sidebar isDisplayUnitLocation />
</Stack>
</Layout.Element>
</Layout>
</section>
</Container>
Expand Down
1 change: 1 addition & 0 deletions src/course-unit/CourseUnit.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@import "./breadcrumbs/Breadcrumbs";
@import "./course-sequence/CourseSequence";
@import "./add-component/AddComponent";
@import "./sidebar/Sidebar";
48 changes: 44 additions & 4 deletions src/course-unit/CourseUnit.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import headerNavigationsMessages from './header-navigations/messages';
import headerTitleMessages from './header-title/messages';
import { getUnitPreviewPath, getUnitViewLivePath } from './utils';
import courseSequenceMessages from './course-sequence/messages';
import messages from './add-component/messages';
import addComponentMessages from './add-component/messages';
import sidebarMessages from './sidebar/messages';
import { extractCourseUnitId } from './sidebar/utils';

let axiosMock;
let store;
Expand Down Expand Up @@ -171,7 +173,7 @@ describe('<CourseUnit />', () => {

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);
Expand All @@ -188,7 +190,7 @@ describe('<CourseUnit />', () => {

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);
Expand Down Expand Up @@ -293,12 +295,50 @@ describe('<CourseUnit />', () => {

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);
expect(mockedUsedNavigate).toHaveBeenCalled();
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`);
});
});

it('renders course unit details for a draft with unpublished changes', async () => {
const { getByText } = render(<RootWrapper />);

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(<RootWrapper />);
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();
});
});
});
18 changes: 18 additions & 0 deletions src/course-unit/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
TextFields as TextFieldsIcon,
VideoCamera as VideoCameraIcon,
} from '@edx/paragon/icons';
import messages from './sidebar/messages';

export const UNIT_ICON_TYPES = ['video', 'other', 'vertical', 'problem', 'lock'];

Expand Down Expand Up @@ -44,3 +45,20 @@ 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 COLORS = {
BLACK: '#000',
GREEN: '#0D7D4D',
};
71 changes: 71 additions & 0 deletions src/course-unit/sidebar/Sidebar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
%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-location-description {
font-size: $font-size-xs;
line-height: $line-height-base;
}

.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;
}
}
29 changes: 29 additions & 0 deletions src/course-unit/sidebar/components/ReleaseInfoComponent.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<span className="course-unit-sidebar-date-and-with">
<h6 className="course-unit-sidebar-date-timestamp m-0 d-inline">
{releaseInfo.releaseDate}&nbsp;
</h6>
{releaseInfo.sectionNameMessage}
</span>
);
}

return releaseInfo.message;

Check warning on line 26 in src/course-unit/sidebar/components/ReleaseInfoComponent.jsx

View check run for this annotation

Codecov / codecov/patch

src/course-unit/sidebar/components/ReleaseInfoComponent.jsx#L26

Added line #L26 was not covered by tests
};

export default ReleaseInfoComponent;
65 changes: 65 additions & 0 deletions src/course-unit/sidebar/components/SidebarBody.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Card, Stack } from '@edx/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, isDisplayUnitLocation, locationId }) => {
const intl = useIntl();
const {
editedOn,
editedBy,
hasChanges,
publishedBy,
publishedOn,
} = useSelector(getCourseUnitData);

return (
<Card.Body className="course-unit-sidebar-date">
<Stack>
{isDisplayUnitLocation ? (
<span>
<h5 className="course-unit-sidebar-date-stage m-0">
{intl.formatMessage(messages.unitLocationTitle)}
</h5>
<p className="m-0 font-weight-bold">
{locationId}
</p>
</span>
) : (
<>
<span>
{getPublishInfo(intl, hasChanges, editedBy, editedOn, publishedBy, publishedOn)}
</span>
<span className="mt-3.5">
<h5 className="course-unit-sidebar-date-stage m-0">
{releaseLabel}
</h5>
<ReleaseInfoComponent />
</span>
<p className="mt-3.5 mb-0">
{intl.formatMessage(messages.sidebarBodyNote)}
</p>
</>
)}
</Stack>
</Card.Body>
);
};

SidebarBody.propTypes = {
releaseLabel: PropTypes.string.isRequired,
isDisplayUnitLocation: PropTypes.bool,
locationId: PropTypes.string,
};

SidebarBody.defaultProps = {
isDisplayUnitLocation: false,
locationId: null,
};

export default SidebarBody;
41 changes: 41 additions & 0 deletions src/course-unit/sidebar/components/SidebarHeader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Icon, Stack } from '@edx/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, isDisplayUnitLocation }) => {
const intl = useIntl();
const { hasChanges, published } = useSelector(getCourseUnitData);
const { iconSrc, colorVariant } = getIconVariant(visibilityState, published, hasChanges);

return (
<Stack className="course-unit-sidebar-header" direction="horizontal">
{!isDisplayUnitLocation && (
<Icon
className="course-unit-sidebar-header-icon"
svgAttrs={{ color: colorVariant }}
src={iconSrc}
/>
)}
<h3 className="course-unit-sidebar-header-title m-0">
{isDisplayUnitLocation ? intl.formatMessage(messages.sidebarHeaderUnitLocationTitle) : title}
</h3>
</Stack>
);
};

SidebarHeader.propTypes = {
title: PropTypes.string.isRequired,
visibilityState: PropTypes.string.isRequired,
isDisplayUnitLocation: PropTypes.bool,
};

SidebarHeader.defaultProps = {
isDisplayUnitLocation: false,
};

export default SidebarHeader;
3 changes: 3 additions & 0 deletions src/course-unit/sidebar/components/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as SidebarHeader } from './SidebarHeader';
export { default as SidebarBody } from './SidebarBody';
export { default as SidebarFooter } from './sidebar-footer';
Loading

0 comments on commit 89db038

Please sign in to comment.