Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [FC-0044] Course unit page - Display sidebar component #832

Merged
merged 10 commits into from
Mar 8, 2024
Merged
Prev Previous commit
Next Next commit
feat: [AXIMST-23] Course unit - Sidebar with unit info (#117)
* 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
commit 55a98888ddc63c02a21f574ecc49708d2fc80de1
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',
};
arbrandes marked this conversation as resolved.
Show resolved Hide resolved
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know if the dates get localized properly in the backend?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date in the backend is always in UTC. If we want it to be in the users time zone we can use <FormattedDate /> and <FormattedTime /> from i18n

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right 💯
I had internal communication on this issue, now we have repeated legacy behavior. Let's take these improvements related to internationalization out of scope.

</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 ? (
arbrandes marked this conversation as resolved.
Show resolved Hide resolved
<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 }) => {
arbrandes marked this conversation as resolved.
Show resolved Hide resolved
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
Loading