diff --git a/package.json b/package.json
index 36cb5b7c7cd..f353e34038a 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/spec/coffeescripts/jsx/dashboard_card/DashboardCardSpec.js b/spec/coffeescripts/jsx/dashboard_card/DashboardCardSpec.js
index eb7573b9202..94c0727781a 100644
--- a/spec/coffeescripts/jsx/dashboard_card/DashboardCardSpec.js
+++ b/spec/coffeescripts/jsx/dashboard_card/DashboardCardSpec.js
@@ -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 =
- 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( )
+
+ 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 =
+test('is accessible', function (assert) {
this.wrapper = $('
').appendTo('body')[0]
- this.component = ReactDOM.render(DashCard, this.wrapper)
- const $html = $(ReactDOM.findDOMNode(this.component))
+ const {container} = render(
, this.wrapper)
+ const $html = $(container.firstChild)
const done = assert.async()
assertions.isAccessible($html, done)
})
-test('unreadCount', function () {
- const DashCard =
- 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(
)
+
+ 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 =
- 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 =
- this.component = TestUtils.renderIntoDocument(DashCard)
- const $html = TestUtils.findRenderedDOMComponentWithClass(
- this.component,
- 'ic-DashboardCard__header_image'
- )
- ok($html, 'image showing')
+ const {getByText} = render(
)
+
+ 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 =
- this.component = TestUtils.renderIntoDocument(DashCard)
- this.component.removeCourseFromFavorites()
+ const {getByText} = render(
)
+ 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 =
- this.component = TestUtils.renderIntoDocument(DashCard)
- this.component.removeCourseFromFavorites()
+ const {getByText} = render(
)
+ 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)
})
diff --git a/spec/javascripts/jsx/dashboard_card/DashboardCardReorderingSpec.js b/spec/javascripts/jsx/dashboard_card/DashboardCardReorderingSpec.js
index 24d8cbfe6ba..30677290b96 100644
--- a/spec/javascripts/jsx/dashboard_card/DashboardCardReorderingSpec.js
+++ b/spec/javascripts/jsx/dashboard_card/DashboardCardReorderingSpec.js
@@ -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(
{
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', () => {
diff --git a/ui/shared/dashboard-card/react/DashboardCard.js b/ui/shared/dashboard-card/react/DashboardCard.js
deleted file mode 100644
index 58ec7ee992a..00000000000
--- a/ui/shared/dashboard-card/react/DashboardCard.js
+++ /dev/null
@@ -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 .
- */
-
-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 (
-
- )
- }
-
- return (
-
- )
-}
-
-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 (
-
- )
- }
- 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 (
-
-
-
{
- this.settingsToggle = c
- }}
- >
-
-
- {I18n.t('Choose a color or course nickname or move course card for %{course}', {
- course: nickname
- })}
-
-
- }
- />
-
- )
- }
-
- render() {
- const dashboardCard = (
- {
- this.cardDiv = c
- }}
- style={{opacity: this.props.isDragging ? 0 : 1}}
- aria-label={this.props.originalName}
- >
-
-
- {this.linksForCard()}
-
-
- )
-
- const {connectDragSource, connectDropTarget} = this.props
- return connectDragSource(connectDropTarget(dashboardCard))
- }
-}
diff --git a/ui/shared/dashboard-card/react/DashboardCard.tsx b/ui/shared/dashboard-card/react/DashboardCard.tsx
new file mode 100644
index 00000000000..ca53b13a39d
--- /dev/null
+++ b/ui/shared/dashboard-card/react/DashboardCard.tsx
@@ -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 .
+ */
+
+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
+}
+
+export const DashboardCardHeaderHero = ({
+ image,
+ backgroundColor,
+ hideColorOverlays,
+ onClick
+}: DashboardCardHeaderHeroProps) => {
+ if (image) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+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()
+
+ 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 (
+
+ )
+ })
+
+ const renderHeaderButton = () => {
+ const reorderingProps = {
+ handleMove,
+ currentPosition: getCardPosition(),
+ lastPosition: totalCards - 1,
+ menuOptions: calculateMenuOptions()
+ }
+
+ return (
+
+
+
handleColorChange(`#${c}`)}
+ currentColor={backgroundColor}
+ nicknameInfo={nicknameInfo}
+ assetString={assetString}
+ onUnfavorite={handleUnfavorite}
+ isFavorited={isFavorited}
+ {...reorderingProps}
+ trigger={
+ {
+ settingsToggle.current = c
+ }}
+ >
+
+
+ {I18n.t('Choose a color or course nickname or move course card for %{course}', {
+ course: nicknameInfo.nickname
+ })}
+
+
+ }
+ />
+
+ )
+ }
+
+ const dashboardCard = (
+
+
+
+ {linksForCard()}
+
+
+ )
+
+ return connectDragSource(connectDropTarget(dashboardCard))
+}
+
+export default DashboardCard
diff --git a/yarn.lock b/yarn.lock
index 77caa6216e3..9357458d028 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"