Convert DashboardCard to TypeScript

refs DE-1083

Change-Id: I2df4456fd58878dbed4366f52e32acdccd141aa5
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/286065
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Aaron Ogata <aogata@instructure.com>
QA-Review: Isaac Moore <isaac.moore@instructure.com>
Product-Review: Isaac Moore <isaac.moore@instructure.com>
This commit is contained in:
Isaac Moore 2022-02-28 15:52:52 -06:00
parent fa4ac3875d
commit 139bfe60a7
6 changed files with 435 additions and 474 deletions

View File

@ -95,6 +95,7 @@
"@sentry/fullstory": "^1.1.7",
"@sentry/react": "^6.16.1",
"@sentry/tracing": "^6.17.2",
"@types/react-dnd": "2.0.36",
"apollo-cache": "^1.3.2",
"apollo-cache-inmemory": "^1.6.3",
"apollo-cache-persist": "^0.1.1",

View File

@ -18,12 +18,10 @@
import $ from 'jquery'
import React from 'react'
import ReactDOM from 'react-dom'
import TestUtils from 'react-dom/test-utils'
import moxios from 'moxios'
import sinon from 'sinon'
import {moxiosWait} from 'jest-moxios-utils'
import {waitFor} from '@testing-library/react'
import {act, cleanup, render, waitFor} from '@testing-library/react'
import DashboardCard from '@canvas/dashboard-card/react/DashboardCard'
import CourseActivitySummaryStore from '@canvas/dashboard-card/react/CourseActivitySummaryStore'
@ -50,6 +48,15 @@ QUnit.module('DashboardCard', {
href: '/courses/1',
courseCode: '101',
id: '1',
links: [
{
css_class: 'discussions',
hidden: false,
icon: 'icon-discussion',
label: 'Discussions',
path: '/courses/1/discussion_topics'
}
],
backgroundColor: '#EF4437',
image: null,
isFavorited: true,
@ -57,12 +64,14 @@ QUnit.module('DashboardCard', {
connectDropTarget: c => c
}
moxios.install()
return sandbox.stub(CourseActivitySummaryStore, 'getStateForCourse').returns({})
return (this.getStateForCourseStub = sandbox
.stub(CourseActivitySummaryStore, 'getStateForCourse')
.returns({}))
},
teardown() {
moxios.uninstall()
localStorage.clear()
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this.component).parentNode)
cleanup()
if (this.wrapper) {
return this.wrapper.remove()
}
@ -75,61 +84,41 @@ function errorRendered() {
}
}
test('render', function () {
const DashCard = <DashboardCard {...this.props} />
this.component = TestUtils.renderIntoDocument(DashCard)
const $html = $(ReactDOM.findDOMNode(this.component))
ok($html.attr('class').match(/DashboardCard/))
const renderSpy = sandbox.spy(this.component, 'render')
ok(!renderSpy.called, 'precondition')
CourseActivitySummaryStore.setState({streams: {1: {stream: this.stream}}})
ok(renderSpy.called, 'should re-render on state update')
test('obtains new course activity when course activity is updated', function (assert) {
const {getByText} = render(<DashboardCard {...this.props} />)
assert.notEqual(getByText(`${this.props.links[0].label} - ${this.props.shortName}`), undefined)
assert.ok(this.getStateForCourseStub.calledOnce)
act(() => CourseActivitySummaryStore.setState({streams: {1: {stream: this.stream}}}))
assert.ok(this.getStateForCourseStub.calledTwice)
})
// eslint-disable-next-line qunit/resolve-async
test('it should be accessible', function (assert) {
const DashCard = <DashboardCard {...this.props} />
test('is accessible', function (assert) {
this.wrapper = $('<div>').appendTo('body')[0]
this.component = ReactDOM.render(DashCard, this.wrapper)
const $html = $(ReactDOM.findDOMNode(this.component))
const {container} = render(<DashboardCard {...this.props} />, this.wrapper)
const $html = $(container.firstChild)
const done = assert.async()
assertions.isAccessible($html, done)
})
test('unreadCount', function () {
const DashCard = <DashboardCard {...this.props} />
this.component = TestUtils.renderIntoDocument(DashCard)
ok(!this.component.unreadCount('icon-discussion', []), 'should not blow up without a stream')
equal(
this.component.unreadCount('icon-discussion', this.stream),
2,
'should pass down unread count if stream item corresponding to icon has unread count'
)
test('does not have an image when a url is not provided', function (assert) {
const {getByText, queryByText} = render(<DashboardCard {...this.props} />)
assert.equal(queryByText(`Course image for ${this.props.shortName}`), undefined)
assert.notEqual(getByText(`Course card color region for ${this.props.shortName}`), undefined)
})
test('does not have image attribute when a url is not provided', function () {
const DashCard = <DashboardCard {...this.props} />
this.component = TestUtils.renderIntoDocument(DashCard)
strictEqual(
TestUtils.scryRenderedDOMComponentsWithClass(this.component, 'ic-DashboardCard__header_image')
.length,
0,
'image attribute should not be present'
)
})
test('has image attribute when url is provided', function () {
test('has an image when a url is provided', function (assert) {
this.props.image = 'http://coolUrl'
const DashCard = <DashboardCard {...this.props} />
this.component = TestUtils.renderIntoDocument(DashCard)
const $html = TestUtils.findRenderedDOMComponentWithClass(
this.component,
'ic-DashboardCard__header_image'
)
ok($html, 'image showing')
const {getByText} = render(<DashboardCard {...this.props} />)
assert.notEqual(getByText(`Course image for ${this.props.shortName}`), undefined)
})
test('#removeCourseFromFavorites succeeds', function () {
test('handles success removing course from favorites', async function (assert) {
const handleRerenderSpy = sinon.spy()
this.props.onConfirmUnfavorite = handleRerenderSpy
@ -139,25 +128,30 @@ test('#removeCourseFromFavorites succeeds', function () {
}
}
const DashCard = <DashboardCard {...this.props} />
this.component = TestUtils.renderIntoDocument(DashCard)
this.component.removeCourseFromFavorites()
const {getByText} = render(<DashboardCard {...this.props} />)
act(() =>
getByText(
`Choose a color or course nickname or move course card for ${this.props.shortName}`
).click()
)
act(() => getByText('Move').click())
act(() => getByText('Unfavorite').click())
act(() => getByText('Submit').click())
return moxiosWait(function () {
await moxiosWait(() => {
const request = moxios.requests.mostRecent()
request.respondWith({
status: 200,
response: []
})
}).then(async function () {
await waitFor(() => waitForResponse())
ok(handleRerenderSpy.calledOnce)
})
await waitFor(() => waitForResponse())
assert.ok(handleRerenderSpy.calledOnce)
})
test('#removeCourseFromFavorites fails', function () {
const handleRerenderSpy = sinon.spy()
this.props.onConfirmUnfavorite = handleRerenderSpy
test('handles failure removing course from favorites', async function (assert) {
this.props.onConfirmUnfavorite = sinon.spy()
function waitForAlert() {
if (errorRendered) {
@ -165,18 +159,24 @@ test('#removeCourseFromFavorites fails', function () {
}
}
const DashCard = <DashboardCard {...this.props} />
this.component = TestUtils.renderIntoDocument(DashCard)
this.component.removeCourseFromFavorites()
const {getByText} = render(<DashboardCard {...this.props} />)
act(() =>
getByText(
`Choose a color or course nickname or move course card for ${this.props.shortName}`
).click()
)
act(() => getByText('Move').click())
act(() => getByText('Unfavorite').click())
act(() => getByText('Submit').click())
return moxiosWait(function () {
await moxiosWait(() => {
const request = moxios.requests.mostRecent()
request.respondWith({
status: 403,
response: []
})
}).then(async function () {
await waitFor(() => waitForAlert())
ok(errorRendered)
})
await waitFor(() => waitForAlert())
assert.ok(errorRendered)
})

View File

@ -24,6 +24,7 @@ import DashboardCard from '@canvas/dashboard-card/react/DashboardCard'
import DraggableDashboardCard from '@canvas/dashboard-card/react/DraggableDashboardCard'
import getDroppableDashboardCardBox from '@canvas/dashboard-card/react/getDroppableDashboardCardBox'
import fakeENV from 'helpers/fakeENV'
import {render} from '@testing-library/react'
let cards
let fakeServer
@ -84,8 +85,8 @@ test('it renders', () => {
ok(root)
})
test('cards have opacity of 0 while moving', () => {
const card = TestUtils.renderIntoDocument(
test('cards have opacity of 0 while moving', assert => {
const {container} = render(
<DashboardCard
cardComponent={DashboardCard}
{...cards[0]}
@ -94,8 +95,8 @@ test('cards have opacity of 0 while moving', () => {
isDragging
/>
)
const div = TestUtils.findRenderedDOMComponentWithClass(card, 'ic-DashboardCard')
equal(div.style.opacity, 0)
assert.equal(container.firstChild.style.opacity, 0)
})
test('moving a card adjusts the position property', () => {

View File

@ -1,403 +0,0 @@
/*
* Copyright (C) 2015 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import { useScope as useI18nScope } from '@canvas/i18n';
import axios from '@canvas/axios'
import DashboardCardAction from './DashboardCardAction'
import CourseActivitySummaryStore from './CourseActivitySummaryStore'
import DashboardCardMenu from './DashboardCardMenu'
import PublishButton from './PublishButton'
import {showConfirmUnfavorite} from './ConfirmUnfavoriteCourseModal'
import {showFlashError} from '@canvas/alerts/react/FlashAlert'
import instFSOptimizedImageUrl from '../util/instFSOptimizedImageUrl'
const I18n = useI18nScope('dashcards');
export function DashboardCardHeaderHero({image, backgroundColor, hideColorOverlays, onClick}) {
if (image) {
return (
<div
className="ic-DashboardCard__header_image"
style={{backgroundImage: `url(${instFSOptimizedImageUrl(image, {x: 262, y: 146})})`}}
>
<div
className="ic-DashboardCard__header_hero"
style={{backgroundColor, opacity: hideColorOverlays ? 0 : 0.6}}
onClick={onClick}
aria-hidden="true"
/>
</div>
)
}
return (
<div
className="ic-DashboardCard__header_hero"
style={{backgroundColor}}
onClick={onClick}
aria-hidden="true"
/>
)
}
export default class DashboardCard extends Component {
// ===============
// CONFIG
// ===============
static propTypes = {
id: PropTypes.string.isRequired,
backgroundColor: PropTypes.string,
shortName: PropTypes.string.isRequired,
originalName: PropTypes.string.isRequired,
courseCode: PropTypes.string.isRequired,
assetString: PropTypes.string.isRequired,
term: PropTypes.string,
href: PropTypes.string.isRequired,
links: PropTypes.arrayOf(PropTypes.object),
image: PropTypes.string,
handleColorChange: PropTypes.func,
hideColorOverlays: PropTypes.bool,
isDragging: PropTypes.bool,
isFavorited: PropTypes.bool,
connectDragSource: PropTypes.func,
connectDropTarget: PropTypes.func,
moveCard: PropTypes.func,
onConfirmUnfavorite: PropTypes.func,
totalCards: PropTypes.number,
position: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),
enrollmentType: PropTypes.string,
observee: PropTypes.string,
published: PropTypes.bool,
canChangeCoursePublishState: PropTypes.bool,
defaultView: PropTypes.string,
pagesUrl: PropTypes.string,
frontPageTitle: PropTypes.string
}
static defaultProps = {
backgroundColor: '#394B58',
term: null,
links: [],
hideColorOverlays: false,
handleColorChange: () => {},
image: '',
isDragging: false,
connectDragSource: c => c,
connectDropTarget: c => c,
moveCard: () => {},
totalCards: 0,
position: 0,
published: false,
canChangeCoursePublishState: false
}
constructor(props) {
super()
this.state = {
nicknameInfo: this.nicknameInfo(props.shortName, props.originalName, props.id),
...CourseActivitySummaryStore.getStateForCourse(props.id)
}
this.removeCourseFromFavorites = this.removeCourseFromFavorites.bind(this)
}
// ===============
// LIFECYCLE
// ===============
componentDidMount() {
CourseActivitySummaryStore.addChangeListener(this.handleStoreChange)
this.parentNode = this.cardDiv
}
componentWillUnmount() {
CourseActivitySummaryStore.removeChangeListener(this.handleStoreChange)
}
// ===============
// ACTIONS
// ===============
settingsClick = e => {
if (e) {
e.preventDefault()
}
this.toggleEditing()
}
getCardPosition() {
return typeof this.props.position === 'function' ? this.props.position() : this.props.position
}
handleNicknameChange = nickname => {
this.setState({
nicknameInfo: this.nicknameInfo(nickname, this.props.originalName, this.props.id)
})
}
handleStoreChange = () => {
this.setState(CourseActivitySummaryStore.getStateForCourse(this.props.id))
}
toggleEditing = () => {
const currentState = !!this.state.editing
this.setState({editing: !currentState})
}
headerClick = e => {
if (e) {
e.preventDefault()
}
window.location = this.props.href
}
doneEditing = () => {
this.setState({editing: false})
this.settingsToggle.focus()
}
handleColorChange = color => {
const hexColor = `#${color}`
this.props.handleColorChange(hexColor)
}
handleMove = (assetString, atIndex) => {
if (typeof this.props.moveCard === 'function') {
this.props.moveCard(assetString, atIndex, () => {
this.settingsToggle.focus()
})
}
}
handleUnfavorite = () => {
const modalProps = {
courseId: this.props.id,
courseName: this.props.originalName,
onConfirm: this.removeCourseFromFavorites,
onClose: this.handleClose,
onEntered: this.handleEntered
}
showConfirmUnfavorite(modalProps)
}
// ===============
// HELPERS
// ===============
nicknameInfo(nickname, originalName, courseId) {
return {
nickname,
originalName,
courseId,
onNicknameChange: this.handleNicknameChange
}
}
unreadCount(icon, stream) {
const activityType = {
'icon-announcement': 'Announcement',
'icon-assignment': 'Message',
'icon-discussion': 'DiscussionTopic'
}[icon]
const itemStream = stream || []
const streamItem = itemStream.find(
item =>
// only return 'Message' type if category is 'Due Date' (for assignments)
item.type === activityType &&
(activityType !== 'Message' || item.notification_category === I18n.t('Due Date'))
)
// TODO: unread count is always 0 for assignments (see CNVS-21227)
return streamItem ? streamItem.unread_count : 0
}
calculateMenuOptions() {
const position = this.getCardPosition()
const isFirstCard = position === 0
const isLastCard = position === this.props.totalCards - 1
return {
canMoveLeft: !isFirstCard,
canMoveRight: !isLastCard,
canMoveToBeginning: !isFirstCard,
canMoveToEnd: !isLastCard
}
}
removeCourseFromFavorites() {
const url = `/api/v1/users/self/favorites/courses/${this.props.id}`
axios
.delete(url)
.then(response => {
if (response.status === 200) {
this.props.onConfirmUnfavorite(this.props.id)
}
})
.catch(() =>
showFlashError(I18n.t('We were unable to remove this course from your favorites.'))
)
}
// ===============
// RENDERING
// ===============
linksForCard() {
return this.props.links.map(link => {
if (!link.hidden) {
const screenReaderLabel = `${link.label} - ${this.state.nicknameInfo.nickname}`
return (
<DashboardCardAction
unreadCount={this.unreadCount(link.icon, this.state.stream)}
iconClass={link.icon}
linkClass={link.css_class}
path={link.path}
screenReaderLabel={screenReaderLabel}
key={link.path}
/>
)
}
return null
})
}
renderHeaderButton() {
const {backgroundColor, hideColorOverlays} = this.props
const reorderingProps = {
handleMove: this.handleMove,
currentPosition: this.getCardPosition(),
lastPosition: this.props.totalCards - 1,
menuOptions: this.calculateMenuOptions()
}
const nickname = this.state.nicknameInfo.nickname
return (
<div>
<div
className="ic-DashboardCard__header-button-bg"
style={{backgroundColor, opacity: hideColorOverlays ? 1 : 0}}
/>
<DashboardCardMenu
afterUpdateColor={this.handleColorChange}
currentColor={this.props.backgroundColor}
nicknameInfo={this.state.nicknameInfo}
assetString={this.props.assetString}
onUnfavorite={this.handleUnfavorite}
isFavorited={this.props.isFavorited}
{...reorderingProps}
trigger={
<button
type="button"
className="Button Button--icon-action-rev ic-DashboardCard__header-button"
ref={c => {
this.settingsToggle = c
}}
>
<i className="icon-more" aria-hidden="true" />
<span className="screenreader-only">
{I18n.t('Choose a color or course nickname or move course card for %{course}', {
course: nickname
})}
</span>
</button>
}
/>
</div>
)
}
render() {
const dashboardCard = (
<div
className="ic-DashboardCard"
ref={c => {
this.cardDiv = c
}}
style={{opacity: this.props.isDragging ? 0 : 1}}
aria-label={this.props.originalName}
>
<div className="ic-DashboardCard__header">
<span className="screenreader-only">
{this.props.image
? I18n.t('Course image for %{course}', {course: this.state.nicknameInfo.nickname})
: I18n.t('Course card color region for %{course}', {
course: this.state.nicknameInfo.nickname
})}
</span>
<DashboardCardHeaderHero
image={this.props.image}
backgroundColor={this.props.backgroundColor}
hideColorOverlays={this.props.hideColorOverlays}
onClick={this.headerClick}
/>
<a href={this.props.href} className="ic-DashboardCard__link">
<div className="ic-DashboardCard__header_content">
<h3
className="ic-DashboardCard__header-title ellipsis"
title={this.props.originalName}
>
<span style={{color: this.props.backgroundColor}}>
{this.state.nicknameInfo.nickname}
</span>
</h3>
<div
className="ic-DashboardCard__header-subtitle ellipsis"
title={this.props.courseCode}
>
{this.props.courseCode}
</div>
<div className="ic-DashboardCard__header-term ellipsis" title={this.props.term}>
{this.props.term ? this.props.term : null}
</div>
{this.props.enrollmentType === 'ObserverEnrollment' && this.props.observee && (
<div className="ic-DashboardCard__header-term ellipsis" title={this.props.observee}>
{I18n.t('Observing: %{observee}', {observee: this.props.observee})}
</div>
)}
</div>
</a>
{!this.props.published && this.props.canChangeCoursePublishState && (
<PublishButton
courseNickname={this.state.nicknameInfo.nickname}
defaultView={this.props.defaultView}
pagesUrl={this.props.pagesUrl}
frontPageTitle={this.props.frontPageTitle}
courseId={this.props.id}
/>
)}
{this.renderHeaderButton()}
</div>
<nav
className="ic-DashboardCard__action-container"
aria-label={I18n.t('Actions for %{course}', {course: this.state.nicknameInfo.nickname})}
>
{this.linksForCard()}
</nav>
</div>
)
const {connectDragSource, connectDropTarget} = this.props
return connectDragSource(connectDropTarget(dashboardCard))
}
}

View File

@ -0,0 +1,355 @@
/*
* Copyright (C) 2015 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, {MouseEventHandler, useCallback, useEffect, useRef, useState} from 'react'
import {useScope as useI18nScope} from '@canvas/i18n'
import axios from '@canvas/axios'
import DashboardCardAction from './DashboardCardAction'
import CourseActivitySummaryStore from './CourseActivitySummaryStore'
import DashboardCardMenu from './DashboardCardMenu'
import PublishButton from './PublishButton'
import {showConfirmUnfavorite} from './ConfirmUnfavoriteCourseModal'
import {showFlashError} from '@canvas/alerts/react/FlashAlert'
import instFSOptimizedImageUrl from '../util/instFSOptimizedImageUrl'
import {ConnectDragSource, ConnectDropTarget} from 'react-dnd'
const I18n = useI18nScope('dashcards')
export type DashboardCardHeaderHeroProps = {
image?: string
backgroundColor?: string
hideColorOverlays?: boolean
onClick?: MouseEventHandler<HTMLElement>
}
export const DashboardCardHeaderHero = ({
image,
backgroundColor,
hideColorOverlays,
onClick
}: DashboardCardHeaderHeroProps) => {
if (image) {
return (
<div
className="ic-DashboardCard__header_image"
style={{backgroundImage: `url(${instFSOptimizedImageUrl(image, {x: 262, y: 146})})`}}
>
<div
className="ic-DashboardCard__header_hero"
style={{backgroundColor, opacity: hideColorOverlays ? 0 : 0.6}}
onClick={onClick}
aria-hidden="true"
/>
</div>
)
}
return (
<div
className="ic-DashboardCard__header_hero"
style={{backgroundColor}}
onClick={onClick}
aria-hidden="true"
/>
)
}
export type DashboardCardProps = {
id: string
backgroundColor?: string
shortName: string
originalName: string
courseCode: string
assetString: string
term?: string
href: string
links: any[] // TODO: improve type
image?: string
handleColorChange?: (color: string) => void
hideColorOverlays?: boolean
isDragging?: boolean
isFavorited?: boolean
connectDragSource?: ConnectDragSource
connectDropTarget?: ConnectDropTarget
moveCard?: (assetString: string, atIndex: number, callback: () => void) => void
onConfirmUnfavorite: (id: string) => void
totalCards?: number
position?: number | (() => number)
enrollmentType?: string
observee?: string
published?: boolean
canChangeCoursePublishState?: boolean
defaultView?: string
pagesUrl?: string
frontPageTitle?: string
}
export const DashboardCard = ({
id,
backgroundColor = '#394B58',
shortName,
originalName,
courseCode,
assetString,
term,
href,
links = [],
image,
handleColorChange = () => {},
hideColorOverlays,
isDragging,
isFavorited,
connectDragSource = c => c,
connectDropTarget = c => c,
moveCard = () => {},
onConfirmUnfavorite,
totalCards = 0,
position = 0,
enrollmentType,
observee,
published,
canChangeCoursePublishState,
defaultView,
pagesUrl,
frontPageTitle
}: DashboardCardProps) => {
const handleNicknameChange = nickname => setNicknameInfo(getNicknameInfo(nickname))
const getNicknameInfo = (nickname: string) => ({
nickname,
originalName,
courseId: id,
onNicknameChange: handleNicknameChange
})
const [nicknameInfo, setNicknameInfo] = useState(getNicknameInfo(shortName))
const [course, setCourse] = useState(CourseActivitySummaryStore.getStateForCourse(id))
const settingsToggle = useRef<HTMLButtonElement | null>()
const handleStoreChange = useCallback(
() => setCourse(CourseActivitySummaryStore.getStateForCourse(id)),
[id]
)
useEffect(() => {
CourseActivitySummaryStore.addChangeListener(handleStoreChange)
return () => CourseActivitySummaryStore.removeChangeListener(handleStoreChange)
}, [handleStoreChange])
// ===============
// ACTIONS
// ===============
const getCardPosition = () => (typeof position === 'function' ? position() : position)
const headerClick: MouseEventHandler = e => {
e.preventDefault()
window.location.assign(href)
}
const handleMove = (asset: string, atIndex: number) => {
if (moveCard) {
moveCard(asset, atIndex, () => settingsToggle.current?.focus())
}
}
const handleUnfavorite = () => {
const modalProps = {
courseId: id,
courseName: originalName,
onConfirm: removeCourseFromFavorites
}
showConfirmUnfavorite(modalProps)
}
// ===============
// HELPERS
// ===============
const unreadCount = (icon: string, stream?: any[]) => {
const activityType = {
'icon-announcement': 'Announcement',
'icon-assignment': 'Message',
'icon-discussion': 'DiscussionTopic'
}[icon]
const itemStream = stream || []
const streamItem = itemStream.find(
item =>
// only return 'Message' type if category is 'Due Date' (for assignments)
item.type === activityType &&
(activityType !== 'Message' || item.notification_category === I18n.t('Due Date'))
)
// TODO: unread count is always 0 for assignments (see CNVS-21227)
return streamItem ? streamItem.unread_count : 0
}
const calculateMenuOptions = () => {
const cardPosition = getCardPosition()
const isFirstCard = cardPosition === 0
const isLastCard = cardPosition === totalCards - 1
return {
canMoveLeft: !isFirstCard,
canMoveRight: !isLastCard,
canMoveToBeginning: !isFirstCard,
canMoveToEnd: !isLastCard
}
}
const removeCourseFromFavorites = () => {
const url = `/api/v1/users/self/favorites/courses/${id}`
axios
.delete(url)
.then(response => {
if (response.status === 200) {
onConfirmUnfavorite(id)
}
})
.catch(() =>
showFlashError(I18n.t('We were unable to remove this course from your favorites.'))
)
}
// ===============
// RENDERING
// ===============
const linksForCard = () =>
links.map(link => {
if (link.hidden) return null
const screenReaderLabel = `${link.label} - ${nicknameInfo.nickname}`
return (
<DashboardCardAction
unreadCount={unreadCount(link.icon, course.stream)}
iconClass={link.icon}
linkClass={link.css_class}
path={link.path}
screenReaderLabel={screenReaderLabel}
key={link.path}
/>
)
})
const renderHeaderButton = () => {
const reorderingProps = {
handleMove,
currentPosition: getCardPosition(),
lastPosition: totalCards - 1,
menuOptions: calculateMenuOptions()
}
return (
<div>
<div
className="ic-DashboardCard__header-button-bg"
style={{backgroundColor, opacity: hideColorOverlays ? 1 : 0}}
/>
<DashboardCardMenu
afterUpdateColor={(c: string) => handleColorChange(`#${c}`)}
currentColor={backgroundColor}
nicknameInfo={nicknameInfo}
assetString={assetString}
onUnfavorite={handleUnfavorite}
isFavorited={isFavorited}
{...reorderingProps}
trigger={
<button
type="button"
className="Button Button--icon-action-rev ic-DashboardCard__header-button"
ref={c => {
settingsToggle.current = c
}}
>
<i className="icon-more" aria-hidden="true" />
<span className="screenreader-only">
{I18n.t('Choose a color or course nickname or move course card for %{course}', {
course: nicknameInfo.nickname
})}
</span>
</button>
}
/>
</div>
)
}
const dashboardCard = (
<div
className="ic-DashboardCard"
style={{opacity: isDragging ? 0 : 1}}
aria-label={originalName}
>
<div className="ic-DashboardCard__header">
<span className="screenreader-only">
{image
? I18n.t('Course image for %{course}', {course: nicknameInfo.nickname})
: I18n.t('Course card color region for %{course}', {
course: nicknameInfo.nickname
})}
</span>
<DashboardCardHeaderHero
image={image}
backgroundColor={backgroundColor}
hideColorOverlays={hideColorOverlays}
onClick={headerClick}
/>
<a href={href} className="ic-DashboardCard__link">
<div className="ic-DashboardCard__header_content">
<h3 className="ic-DashboardCard__header-title ellipsis" title={originalName}>
<span style={{color: backgroundColor}}>{nicknameInfo.nickname}</span>
</h3>
<div className="ic-DashboardCard__header-subtitle ellipsis" title={courseCode}>
{courseCode}
</div>
<div className="ic-DashboardCard__header-term ellipsis" title={term}>
{term || null}
</div>
{enrollmentType === 'ObserverEnrollment' && observee && (
<div className="ic-DashboardCard__header-term ellipsis" title={observee}>
{I18n.t('Observing: %{observee}', {observee})}
</div>
)}
</div>
</a>
{!published && canChangeCoursePublishState && (
<PublishButton
courseNickname={nicknameInfo.nickname}
defaultView={defaultView}
pagesUrl={pagesUrl}
frontPageTitle={frontPageTitle}
courseId={id}
/>
)}
{renderHeaderButton()}
</div>
<nav
className="ic-DashboardCard__action-container"
aria-label={I18n.t('Actions for %{course}', {course: nicknameInfo.nickname})}
>
{linksForCard()}
</nav>
</div>
)
return connectDragSource(connectDropTarget(dashboardCard))
}
export default DashboardCard

View File

@ -5742,6 +5742,13 @@
dependencies:
"@types/react" "*"
"@types/react-dnd@2.0.36":
version "2.0.36"
resolved "https://registry.yarnpkg.com/@types/react-dnd/-/react-dnd-2.0.36.tgz#67e08a3608f112a3af27201d1fb6f79334d43214"
integrity sha512-jA95HjQxuHNSnr0PstVBjRwVcFJZoinxbtsS4bpi5nwAL5GUOtjrLrq1bDi4WNYxW+77KHvqSAZ2EgA2q9evdA==
dependencies:
"@types/react" "*"
"@types/react-dom@>=16.9.0", "@types/react-dom@^17.0.9":
version "17.0.9"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
@ -21783,9 +21790,9 @@ redux@^3.6.0, redux@^3.7.1, redux@^3.7.2:
symbol-observable "^1.0.3"
redux@^4, redux@^4.0.1:
version "4.1.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4"
integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==
version "4.1.2"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104"
integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==
dependencies:
"@babel/runtime" "^7.9.2"