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 29, 2024
1 parent 62ae4b5 commit 55a9888
Show file tree
Hide file tree
Showing 30 changed files with 729 additions and 343 deletions.
14 changes: 10 additions & 4 deletions src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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 @@ -86,9 +87,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 @@ -110,7 +111,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
Expand Up @@ -2,3 +2,4 @@
@import "./course-sequence/CourseSequence";
@import "./add-component/AddComponent";
@import "./course-xblock/CourseXblock";
@import "./sidebar/Sidebar";
40 changes: 40 additions & 0 deletions src/course-unit/CourseUnit.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import CourseUnit from './CourseUnit';
import headerNavigationsMessages from './header-navigations/messages';
import headerTitleMessages from './header-title/messages';
import courseSequenceMessages from './course-sequence/messages';
import sidebarMessages from './sidebar/messages';
import { extractCourseUnitId } from './sidebar/utils';

import deleteModalMessages from '../generic/delete-modal/messages';
import courseXBlockMessages from './course-xblock/messages';
Expand Down Expand Up @@ -310,6 +312,44 @@ describe('<CourseUnit />', () => {
});
});

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();
});
});

it('checks whether xblock is deleted when corresponding delete button is clicked', async () => {
axiosMock
.onDelete(getXBlockBaseApiUrl(courseVerticalChildrenMock.children[0].block_id))
Expand Down
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 '@openedx/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 '@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, 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 '@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, 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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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 = () => {
const intl = useIntl();
const {
published,
hasChanges,
enableCopyPasteUnits,
} = useSelector(getCourseUnitData);

return (
<>
{(!published || hasChanges) && (
<Button
className="mt-3.5"
variant="outline-primary"
size="sm"
>
{intl.formatMessage(messages.actionButtonPublishTitle)}
</Button>
)}
{(published && hasChanges) && (
<Button
className="mt-2"
variant="link"
size="sm"
>
{intl.formatMessage(messages.actionButtonDiscardChangesTitle)}
</Button>
)}
{enableCopyPasteUnits && (
<Button

Check warning on line 37 in src/course-unit/sidebar/components/sidebar-footer/ActionButtons.jsx

View check run for this annotation

Codecov / codecov/patch

src/course-unit/sidebar/components/sidebar-footer/ActionButtons.jsx#L37

Added line #L37 was not covered by tests
className="mt-2"
variant="outline-primary"
size="sm"
>
{intl.formatMessage(messages.actionButtonCopyUnitTitle)}
</Button>
)}
</>
);
};

export default ActionButtons;
Loading

0 comments on commit 55a9888

Please sign in to comment.