Rearrange discussion layout.
This takes the functionality in CourseItemRow and instead implements the required parts in DiscussionRow. This amounts to a complete rewrite of CourseItemRow, and as such will require thorough regression QA. I made a bit of an effort to keep things shareable between the two but it wasn't all working and I gave up to get this out. In time we should extract small shareable pieces between this and announcements and then refactor AnnouncementRow to no longer use CourseItemRow. Also leverage INSTUI better to not have so much manual CSS. Closes COMMS-1059 Test Plan: * Compare page to designs at [designs link] make sure the appearance matches. Well some of it, since some of stuff they're asking for doesn't really make sense (i.e. having assignments with specific sections) * Things should still work. Since this was a substantial change and refactoring, please test this thoroughly. * I didn't touch CourseItemRow or AnnouncementRow, but it wouldn't be a terrible idea to smoketest. Change-Id: I86e2e4f9ab5deeb3a4fe51d63da199c94321b8c5 Reviewed-on: https://gerrit.instructure.com/148195 Tested-by: Jenkins Reviewed-by: Landon Gilbert-Bland <lbland@instructure.com> Reviewed-by: Aaron Kc Hsu <ahsu@instructure.com> QA-Review: Aaron Kc Hsu <ahsu@instructure.com> Product-Review: Kendall Chadwick <kchadwick@instructure.com>
This commit is contained in:
parent
aed929745b
commit
41ec03439a
|
@ -24,19 +24,25 @@ import { connect } from 'react-redux'
|
|||
import { DragSource, DropTarget } from 'react-dnd';
|
||||
import { findDOMNode } from 'react-dom'
|
||||
import { func, bool, string, arrayOf } from 'prop-types'
|
||||
import cx from 'classnames'
|
||||
|
||||
import $ from 'jquery'
|
||||
import 'jquery.instructure_date_and_time'
|
||||
|
||||
import Badge from '@instructure/ui-core/lib/components/Badge'
|
||||
import Container from '@instructure/ui-core/lib/components/Container'
|
||||
import Grid, { GridCol, GridRow} from '@instructure/ui-core/lib/components/Grid'
|
||||
import Flex, { FlexItem } from '@instructure/ui-layout/lib/components/Flex'
|
||||
import Grid, { GridCol, GridRow} from '@instructure/ui-layout/lib/components/Grid'
|
||||
import Heading from '@instructure/ui-core/lib/components/Heading'
|
||||
|
||||
import IconAssignmentLine from 'instructure-icons/lib/Line/IconAssignmentLine'
|
||||
import IconBookmarkLine from 'instructure-icons/lib/Line/IconBookmarkLine'
|
||||
import IconBookmarkSolid from 'instructure-icons/lib/Solid/IconBookmarkSolid'
|
||||
import IconCopySolid from 'instructure-icons/lib/Solid/IconCopySolid'
|
||||
import IconDragHandleLine from 'instructure-icons/lib/Line/IconDragHandleLine'
|
||||
import IconLock from 'instructure-icons/lib/Line/IconLockLine'
|
||||
import IconLtiLine from 'instructure-icons/lib/Line/IconLtiLine'
|
||||
import IconPeerReviewLine from 'instructure-icons/lib/Line/IconPeerReviewLine'
|
||||
import IconPinLine from 'instructure-icons/lib/Line/IconPinLine'
|
||||
import IconPinSolid from 'instructure-icons/lib/Solid/IconPinSolid'
|
||||
import IconPublishSolid from 'instructure-icons/lib/Solid/IconPublishSolid'
|
||||
|
@ -44,15 +50,19 @@ import IconTrashSolid from 'instructure-icons/lib/Solid/IconTrashSolid'
|
|||
import IconUnlock from 'instructure-icons/lib/Line/IconUnlockLine'
|
||||
import IconUnpublishedLine from 'instructure-icons/lib/Line/IconUnpublishedLine'
|
||||
import IconUpdownLine from 'instructure-icons/lib/Line/IconUpdownLine'
|
||||
import Pill from '@instructure/ui-core/lib/components/Pill'
|
||||
import ScreenReaderContent from '@instructure/ui-core/lib/components/ScreenReaderContent'
|
||||
import Text from '@instructure/ui-core/lib/components/Text'
|
||||
import { MenuItem } from '@instructure/ui-core/lib/components/Menu'
|
||||
|
||||
import DiscussionModel from 'compiled/models/DiscussionTopic'
|
||||
import LockIconView from 'compiled/views/LockIconView'
|
||||
|
||||
import actions from '../actions'
|
||||
import compose from '../../shared/helpers/compose'
|
||||
import CourseItemRow from '../../shared/components/CourseItemRow'
|
||||
import CyoeHelper from '../../shared/conditional_release/CyoeHelper'
|
||||
import DiscussionManageMenu from '../../shared/components/DiscussionManageMenu'
|
||||
import discussionShape from '../../shared/proptypes/discussion'
|
||||
import masterCourseDataShape from '../../shared/proptypes/masterCourseData'
|
||||
import propTypes from '../propTypes'
|
||||
|
@ -60,7 +70,7 @@ import SectionsTooltip from '../../shared/SectionsTooltip'
|
|||
import select from '../../shared/select'
|
||||
import ToggleIcon from '../../shared/components/ToggleIcon'
|
||||
import UnreadBadge from '../../shared/components/UnreadBadge'
|
||||
import { makeTimestamp } from '../../shared/date-utils'
|
||||
import { isPassedDelayedPostAt } from '../../shared/date-utils'
|
||||
|
||||
const dragTarget = {
|
||||
beginDrag (props) {
|
||||
|
@ -111,6 +121,9 @@ export class DiscussionRow extends Component {
|
|||
displayDuplicateMenuItem: bool.isRequired,
|
||||
displayLockMenuItem: bool.isRequired,
|
||||
displayMasteryPathsMenuItem: bool,
|
||||
displayMasteryPathsLink: bool,
|
||||
displayMasteryPathsPill:bool,
|
||||
masteryPathsPillLabel: string, // required if displayMasteryPathsPill is true
|
||||
displayManageMenu: bool.isRequired,
|
||||
displayPinMenuItem: bool.isRequired,
|
||||
draggable: bool,
|
||||
|
@ -134,12 +147,42 @@ export class DiscussionRow extends Component {
|
|||
isDragging: false,
|
||||
masterCourseData: null,
|
||||
displayMasteryPathsMenuItem: false,
|
||||
displayMasteryPathsLink: false,
|
||||
displayMasteryPathsPill: false,
|
||||
masteryPathsPillLabel: "",
|
||||
moveCard: () => {},
|
||||
onMoveDiscussion: null,
|
||||
onSelectedChanged () {},
|
||||
rowRef () {},
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
this.onFocusManage(this.props)
|
||||
}
|
||||
|
||||
componentWillReceiveProps = (nextProps) => {
|
||||
this.onFocusManage(nextProps)
|
||||
}
|
||||
|
||||
// TODO: Move this to a common file so announcements can use this also.
|
||||
onFocusManage = (props) => {
|
||||
if (props.discussion.focusOn) {
|
||||
switch (props.discussion.focusOn) {
|
||||
case 'title':
|
||||
this._titleElement.focus()
|
||||
break;
|
||||
case 'manageMenu':
|
||||
this._manageMenu.focus()
|
||||
break;
|
||||
case 'toggleButton':
|
||||
break;
|
||||
default:
|
||||
throw new Error(I18n.t('Illegal element focus request'))
|
||||
}
|
||||
this.props.cleanDiscussionFocus()
|
||||
}
|
||||
}
|
||||
|
||||
onManageDiscussion = (e, { action, id, menuTool }) => {
|
||||
switch (action) {
|
||||
case 'duplicate':
|
||||
|
@ -244,8 +287,7 @@ export class DiscussionRow extends Component {
|
|||
}
|
||||
onToggleOn={() => this.props.updateDiscussion(this.props.discussion, {published: true}, {})}
|
||||
onToggleOff={() => this.props.updateDiscussion(this.props.discussion, {published: false}, {})}
|
||||
className="publish-button"
|
||||
/>)
|
||||
className="publish-button"/>)
|
||||
: null
|
||||
)
|
||||
|
||||
|
@ -388,17 +430,225 @@ export class DiscussionRow extends Component {
|
|||
return menuList
|
||||
}
|
||||
|
||||
renderDragHandleIfAppropriate = () => {
|
||||
if (this.props.draggable && this.props.connectDragSource) {
|
||||
return (
|
||||
<div className="ic-item-row__drag-col">
|
||||
<span>
|
||||
<Text color="secondary" size="large">
|
||||
<IconDragHandleLine />
|
||||
</Text>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
renderSectionsTooltip = () => {
|
||||
if (this.props.contextType === "group" || this.props.discussion.assignment ||
|
||||
this.props.discussion.group_category_id) {
|
||||
this.props.discussion.group_category_id) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionsTooltip
|
||||
totalUserCount={this.props.discussion.user_count}
|
||||
sections={this.props.discussion.sections}
|
||||
/>
|
||||
<GridRow vAlign="middle">
|
||||
<GridCol textAlign="start">
|
||||
<SectionsTooltip
|
||||
totalUserCount={this.props.discussion.user_count}
|
||||
sections={this.props.discussion.sections}
|
||||
/>
|
||||
</GridCol>
|
||||
</GridRow>
|
||||
)
|
||||
}
|
||||
|
||||
renderTitle = () => {
|
||||
const refFn = (c) => { this._titleElement = c }
|
||||
const linkUrl = this.props.discussion.html_url
|
||||
return (
|
||||
<div className="ic-item-row__content-col">
|
||||
<Heading level="h3" margin="0">
|
||||
<a style={{color:"inherit"}} className="discussion-title" ref={refFn} href={linkUrl}>
|
||||
{this.props.discussion.title}
|
||||
</a>
|
||||
</Heading>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderLastReplyAt = () => {
|
||||
const datetimeString = $.datetimeString(this.props.discussion.last_reply_at)
|
||||
return (
|
||||
<div className="ic-item-row__content-col ic-discussion-row__content last-reply-at">
|
||||
{ I18n.t('Last post at %{date}', { date: datetimeString }) }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderDueDate = () => {
|
||||
const assignment = this.props.discussion.assignment // eslint-disable-line
|
||||
const dueDateString = assignment && assignment.due_at
|
||||
? I18n.t('Due %{date}', { date: $.datetimeString(assignment.due_at) })
|
||||
: null
|
||||
return (
|
||||
<div className="ic-discussion-row__content">
|
||||
{ dueDateString }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderAvailabilityDate = () => {
|
||||
const availabilityBegin = this.props.discussion.delayed_post_at
|
||||
const availabilityEnd = this.props.discussion.lock_at
|
||||
|
||||
// Check if we are too early for the topic to be available
|
||||
if (availabilityBegin && !isPassedDelayedPostAt({ checkDate: null, delayedDate: availabilityBegin })) {
|
||||
return (
|
||||
<div className="discussion-delayed-until ic-item-row__content-col ic-discussion-row__content">
|
||||
{ I18n.t('Not available until %{date}',
|
||||
{date: $.datetimeString(availabilityBegin)})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (availabilityEnd) {
|
||||
if (isPassedDelayedPostAt({ checkDate: null, delayedDate: availabilityEnd })) {
|
||||
return (
|
||||
<div className="discussion-was-locked ic-item-row__content-col ic-discussion-row__content">
|
||||
{I18n.t('Was locked at %{date}',
|
||||
{date: $.datetimeString(availabilityEnd)})}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className="discussion-available-until ic-item-row__content-col ic-discussion-row__content">
|
||||
{I18n.t('Available until %{date}',
|
||||
{date: $.datetimeString(availabilityEnd)})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
unmountMasterCourseLock = () => {
|
||||
if (this.masterCourseLock) {
|
||||
this.masterCourseLock.remove()
|
||||
this.masterCourseLock = null
|
||||
}
|
||||
}
|
||||
|
||||
initializeMasterCourseIcon = (container) => {
|
||||
const masterCourse = {
|
||||
courseData: this.props.masterCourseData || {},
|
||||
getLockOptions: () => ({
|
||||
model: new DiscussionModel(this.props.discussion),
|
||||
unlockedText: I18n.t('%{title} is unlocked. Click to lock.', {title: this.props.discussion.title}),
|
||||
lockedText: I18n.t('%{title} is locked. Click to unlock', {title: this.props.discussion.title}),
|
||||
course_id: this.props.masterCourseData.masterCourse.id,
|
||||
content_id: this.props.discussion.id,
|
||||
content_type: 'discussion_topic',
|
||||
}),
|
||||
}
|
||||
const { courseData = {}, getLockOptions } = masterCourse || {}
|
||||
if (container && (courseData.isMasterCourse || courseData.isChildCourse)) {
|
||||
this.unmountMasterCourseLock()
|
||||
const opts = getLockOptions()
|
||||
|
||||
// initialize master course lock icon, which is a Backbone view
|
||||
// I know, I know, backbone in react is grosssss but wachagunnado
|
||||
this.masterCourseLock = new LockIconView({ ...opts, el: container })
|
||||
this.masterCourseLock.render()
|
||||
}
|
||||
}
|
||||
|
||||
renderUpperRightBadges = () => {
|
||||
const assignment = this.props.discussion.assignment // eslint-disable-line
|
||||
const peerReview = assignment ? assignment.peer_reviews : false
|
||||
const maybeRenderPeerReviewIcon = peerReview ? (
|
||||
<span className="ic-item-row__peer_review">
|
||||
<Text color="success" size="medium">
|
||||
<IconPeerReviewLine />
|
||||
</Text>
|
||||
</span>
|
||||
) : null
|
||||
const maybeDisplayManageMenu = this.props.displayManageMenu ? (
|
||||
<span display="inline-block">
|
||||
<DiscussionManageMenu
|
||||
menuRefFn = {(c) => {this._manageMenu = c }}
|
||||
onSelect={this.onManageDiscussion}
|
||||
entityTitle={this.props.discussion.title}
|
||||
menuOptions={this.renderMenuList} />
|
||||
</span>
|
||||
) : null
|
||||
const returnTo = encodeURIComponent(window.location.pathname)
|
||||
const discussionId = this.props.discussion.id
|
||||
const maybeRenderMasteryPathsPill = this.props.displayMasteryPathsPill ? (
|
||||
<span display="inline-block" className="discussion-row-mastery-paths-pill">
|
||||
<Pill text={this.props.masteryPathsPillLabel} />
|
||||
</span>
|
||||
) : null
|
||||
const maybeRenderMasteryPathsLink = this.props.displayMasteryPathsLink ? (
|
||||
<a href={`discussion_topics/${discussionId}/edit?return_to=${returnTo}#mastery-paths-editor`}
|
||||
className="discussion-index-mastery-paths-link">
|
||||
{I18n.t('Mastery Paths')}
|
||||
</a>
|
||||
) : null
|
||||
const actionsContent = [this.readCount(), this.publishButton(), this.subscribeButton()]
|
||||
return (
|
||||
<div>
|
||||
<div className="ic-item-row__meta-actions">
|
||||
{maybeRenderMasteryPathsPill}
|
||||
{maybeRenderMasteryPathsLink}
|
||||
{maybeRenderPeerReviewIcon}
|
||||
{actionsContent}
|
||||
<span ref={this.initializeMasterCourseIcon} className="ic-item-row__master-course-lock" />
|
||||
{maybeDisplayManageMenu}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderDiscussion = () => {
|
||||
const classes = cx('ic-item-row')
|
||||
return (
|
||||
this.props.connectDropTarget(this.props.connectDragSource(
|
||||
<div style={{ opacity: (this.props.isDragging) ? 0 : 1 }} className={`${classes} ic-discussion-row`}>
|
||||
<Flex width="100%">
|
||||
<FlexItem shrink padding="xx-small">
|
||||
{this.renderDragHandleIfAppropriate()}
|
||||
</FlexItem>
|
||||
<FlexItem shrink padding="xx-small">
|
||||
{this.renderIcon()}
|
||||
</FlexItem>
|
||||
<FlexItem padding="xx-small" grow shrink>
|
||||
<Grid startAt="medium" vAlign="middle" rowSpacing="none" colSpacing="none">
|
||||
<GridRow vAlign="middle">
|
||||
<GridCol textAlign="start">
|
||||
{this.renderTitle()}
|
||||
</GridCol>
|
||||
<GridCol textAlign="end">
|
||||
{this.renderUpperRightBadges()}
|
||||
</GridCol>
|
||||
</GridRow>
|
||||
{this.renderSectionsTooltip()} {/* This is wrapped in a GridRow if present */}
|
||||
<GridRow>
|
||||
<GridCol textAlign="start">
|
||||
{this.renderLastReplyAt()}
|
||||
</GridCol>
|
||||
<GridCol textAlign="center">
|
||||
{this.renderAvailabilityDate()}
|
||||
</GridCol>
|
||||
<GridCol textAlign="end">
|
||||
{this.renderDueDate()}
|
||||
</GridCol>
|
||||
</GridRow>
|
||||
</Grid>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</div>, {dropEffect: 'copy'}
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -423,62 +673,7 @@ export class DiscussionRow extends Component {
|
|||
</Container>}
|
||||
</GridCol>
|
||||
<GridCol>
|
||||
<CourseItemRow
|
||||
ref={this.props.rowRef}
|
||||
className="ic-discussion-row"
|
||||
key={this.props.discussion.id}
|
||||
id={this.props.discussion.id}
|
||||
isDragging={this.props.isDragging}
|
||||
focusOn={this.props.discussion.focusOn}
|
||||
draggable={this.props.draggable}
|
||||
connectDragSource={this.props.connectDragSource}
|
||||
connectDropTarget={this.props.connectDropTarget}
|
||||
icon={this.renderIcon() }
|
||||
isRead
|
||||
author={this.props.discussion.author}
|
||||
title={this.props.discussion.title}
|
||||
body={textContent ? <div className="ic-discussion-row__content">{textContent}</div> : null}
|
||||
sectionToolTip={this.renderSectionsTooltip()}
|
||||
itemUrl={this.props.discussion.html_url}
|
||||
onSelectedChanged={this.props.onSelectedChanged}
|
||||
peerReview={this.props.discussion.assignment ? this.props.discussion.assignment.peer_reviews : false}
|
||||
showManageMenu={this.props.displayManageMenu}
|
||||
onManageMenuSelect={this.onManageDiscussion}
|
||||
clearFocusDirectives={this.props.cleanDiscussionFocus}
|
||||
manageMenuOptions={this.renderMenuList}
|
||||
masterCourse={{
|
||||
courseData: this.props.masterCourseData || {},
|
||||
getLockOptions: () => ({
|
||||
model: new DiscussionModel(this.props.discussion),
|
||||
unlockedText: I18n.t('%{title} is unlocked. Click to lock.', {title: this.props.discussion.title}),
|
||||
lockedText: I18n.t('%{title} is locked. Click to unlock', {title: this.props.discussion.title}),
|
||||
course_id: this.props.masterCourseData.masterCourse.id,
|
||||
content_id: this.props.discussion.id,
|
||||
content_type: 'discussion_topic',
|
||||
}),
|
||||
}}
|
||||
metaContent={
|
||||
<div>
|
||||
<span className="ic-item-row__meta-content-heading">
|
||||
<Text size="small" as="p">{
|
||||
makeTimestamp(this.props.discussion,
|
||||
delayedLabel,
|
||||
postedAtLabel
|
||||
).title
|
||||
}</Text>
|
||||
</span>
|
||||
<Text color="secondary" size="small" as="p">
|
||||
{$.datetimeString(
|
||||
makeTimestamp(this.props.discussion,
|
||||
delayedLabel,
|
||||
postedAtLabel
|
||||
).date, {format: 'medium'}
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
actionsContent={[this.readCount(), this.publishButton(), this.subscribeButton()]}
|
||||
/>
|
||||
{this.renderDiscussion()}
|
||||
</GridCol>
|
||||
</GridRow>
|
||||
</Grid>
|
||||
|
@ -500,6 +695,8 @@ const mapDispatch = (dispatch) => {
|
|||
const mapState = (state, ownProps) => {
|
||||
const { discussion } = ownProps
|
||||
const cyoe = CyoeHelper.getItemData(discussion.assignment_id)
|
||||
const shouldShowMasteryPathsPill = cyoe.isReleased && cyoe.releasedLabel &&
|
||||
(cyoe.releasedLabel !== "") && discussion.permissions.update
|
||||
const propsFromState = {
|
||||
canPublish: state.permissions.publish,
|
||||
contextType: state.contextType,
|
||||
|
@ -508,6 +705,9 @@ const mapState = (state, ownProps) => {
|
|||
displayDuplicateMenuItem: state.permissions.manage_content,
|
||||
displayLockMenuItem: discussion.can_lock,
|
||||
displayMasteryPathsMenuItem: cyoe.isCyoeAble,
|
||||
displayMasteryPathsLink: cyoe.isTrigger && discussion.permissions.update,
|
||||
displayMasteryPathsPill: shouldShowMasteryPathsPill,
|
||||
masteryPathsPillLabel: cyoe.releasedLabel,
|
||||
displayManageMenu: discussion.permissions.delete,
|
||||
displayPinMenuItem: state.permissions.moderate,
|
||||
masterCourseData: state.masterCourseData,
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
// TODO: Get rid of this component. AnnouncementRow should manage its own layout
|
||||
// with the shared utilities created in g/something.
|
||||
import I18n from 'i18n!shared_components'
|
||||
import React, { Component } from 'react'
|
||||
import { bool, node, string, func, shape, arrayOf, oneOf } from 'prop-types'
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 I18n from 'i18n!shared_components'
|
||||
import React, { Component } from 'react'
|
||||
import { func, string } from 'prop-types'
|
||||
|
||||
import Button from '@instructure/ui-core/lib/components/Button'
|
||||
import IconMore from 'instructure-icons/lib/Line/IconMoreLine'
|
||||
import PopoverMenu from '@instructure/ui-core/lib/components/PopoverMenu'
|
||||
import ScreenReaderContent from '@instructure/ui-core/lib/components/ScreenReaderContent'
|
||||
|
||||
export default class DiscussionManageMenu extends Component {
|
||||
static propTypes = {
|
||||
onSelect: func.isRequired,
|
||||
// This should be a *function* that returns an array of MenuList
|
||||
// components; this way we don't actually create a gargantuan array
|
||||
// of things until we actually need them (i.e. when this menu is
|
||||
// clicked). This somewhat benefits performance in discussions, where
|
||||
// we might have hundreds on the page.
|
||||
menuOptions: func.isRequired,
|
||||
entityTitle: string.isRequired,
|
||||
// Use this if you want the calling component to have a handle to this menu
|
||||
menuRefFn: func
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
menuRefFn: (_) => {}
|
||||
}
|
||||
|
||||
state = {
|
||||
manageMenuOpen: false
|
||||
}
|
||||
|
||||
toggleManageMenuOpen = (shown, _) => {
|
||||
this.setState({ manageMenuOpen: shown })
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<span className="ic-item-row__manage-menu">
|
||||
<PopoverMenu
|
||||
ref={this.props.menuRefFn}
|
||||
onSelect={this.props.onSelect}
|
||||
onToggle={this.toggleManageMenuOpen}
|
||||
trigger={
|
||||
<Button variant="icon" size="small">
|
||||
<IconMore />
|
||||
<ScreenReaderContent>{I18n.t('Manage options for %{name}', { name: this.props.entityTitle })}</ScreenReaderContent>
|
||||
</Button>
|
||||
}>
|
||||
{this.state.manageMenuOpen ? this.props.menuOptions() : null}
|
||||
</PopoverMenu>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ export default function ToggleIcon ({ toggled, OnIcon, OffIcon, onToggleOn,
|
|||
return (
|
||||
<span className={className}>
|
||||
<Button
|
||||
variant="icon"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
theme={{borderWidth: "0"}}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -188,3 +188,24 @@
|
|||
margin: direction-sides(0 -8px -8px -10px);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.discussion-title {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
@include ic-focus-base;
|
||||
&:focus {
|
||||
@include ic-focus-variant;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We should try to not need this CSS
|
||||
.discussion-row-mastery-paths-pill {
|
||||
margin-#{direction(left)}: 6px;
|
||||
margin-#{direction(right)}: 6px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ const makeProps = (props = {}) => _.merge({
|
|||
locked: false,
|
||||
html_url: '',
|
||||
user_count: 10,
|
||||
last_reply_at: new Date(2018, 1, 14, 0, 0, 0, 0)
|
||||
},
|
||||
canPublish: false,
|
||||
masterCourseData: {},
|
||||
|
@ -134,19 +135,57 @@ test('renders the publish ToggleIcon', () => {
|
|||
})
|
||||
|
||||
test('renders "Delayed until" date label if discussion is delayed', () => {
|
||||
const discussion = { delayed_post_at: (new Date).toString() }
|
||||
const delayedDate = new Date
|
||||
delayedDate.setYear(delayedDate.getFullYear() + 1)
|
||||
const discussion = { delayed_post_at: delayedDate.toString() }
|
||||
const tree = mount(<DiscussionRow {...makeProps({ discussion })} />)
|
||||
const node = tree.find('.ic-item-row__meta-content-heading')
|
||||
const node = tree.find('.discussion-delayed-until')
|
||||
ok(node.exists())
|
||||
})
|
||||
|
||||
test('renders "Posted on" date label if discussion is not delayed', () => {
|
||||
const discussion = { delayed_post_at: null }
|
||||
const tree = mount(<DiscussionRow {...makeProps({ discussion })} />)
|
||||
const node = tree.find('.ic-item-row__meta-content-heading')
|
||||
ok(node.text().includes('Posted on'))
|
||||
test('renders a last reply at date', () => {
|
||||
const tree = mount(<DiscussionRow {...makeProps()} />)
|
||||
const node = tree.find('.last-reply-at')
|
||||
ok(node.exists())
|
||||
ok(node.text().includes('Last post at'))
|
||||
ok(node.text().includes('Feb'))
|
||||
})
|
||||
|
||||
test('renders available until if approprate', () => {
|
||||
const futureDate = new Date
|
||||
futureDate.setYear(futureDate.getFullYear() + 1)
|
||||
const discussion = { lock_at: futureDate }
|
||||
const tree = mount(<DiscussionRow {...makeProps({ discussion })} />)
|
||||
const node = tree.find('.discussion-available-until')
|
||||
ok(node.exists())
|
||||
ok(node.text().includes('Available until'))
|
||||
// We need a relative date to ensure future-ness, so we can't really insist
|
||||
// on a given date element appearing this time
|
||||
})
|
||||
|
||||
test('renders locked at if appropriate', () => {
|
||||
const pastDate = new Date
|
||||
pastDate.setYear(pastDate.getFullYear() - 1)
|
||||
const discussion = { lock_at: pastDate }
|
||||
const tree = mount(<DiscussionRow {...makeProps({ discussion })} />)
|
||||
const node = tree.find('.discussion-was-locked')
|
||||
ok(node.exists())
|
||||
ok(node.text().includes('Was locked at'))
|
||||
// We need a relative date to ensure past-ness, so we can't really insist
|
||||
// on a given date element appearing this time
|
||||
})
|
||||
|
||||
test('renders nothing if currently available and no end date', () => {
|
||||
const tree = mount(<DiscussionRow {...makeProps()} />)
|
||||
let node = tree.find('.discussion-available-until')
|
||||
notOk(node.exists())
|
||||
node = tree.find('.discussion-delayed-until')
|
||||
notOk(node.exists())
|
||||
node = tree.find('.discussion-was-locked')
|
||||
notOk(node.exists())
|
||||
})
|
||||
|
||||
|
||||
test('renders the SectionsTooltip component', () => {
|
||||
const discussion = { user_count: 200 }
|
||||
const tree = mount(<DiscussionRow {...makeProps({ discussion })} />)
|
||||
|
@ -162,14 +201,14 @@ test('renders the SectionsTooltip component with sections', () => {
|
|||
equal(tree.find('SectionsTooltip Text').text(), '2 Sectionssection 4section 2')
|
||||
})
|
||||
|
||||
test('does not renders the SectionsTooltip component on a graded discussion', () => {
|
||||
test('does not render the SectionsTooltip component on a graded discussion', () => {
|
||||
const discussion = { user_count: 200, assignment: true }
|
||||
const tree = mount(<DiscussionRow {...makeProps({ discussion })} />)
|
||||
const node = tree.find('SectionsTooltip')
|
||||
notOk(node.exists())
|
||||
})
|
||||
|
||||
test('does not renders the SectionsTooltip component on a group discussion', () => {
|
||||
test('does not render the SectionsTooltip component on a group discussion', () => {
|
||||
const discussion = { user_count: 200, group_category_id: 13 }
|
||||
const tree = mount(<DiscussionRow {...makeProps({ discussion })} />)
|
||||
const node = tree.find('SectionsTooltip')
|
||||
|
@ -181,26 +220,27 @@ test('does not renders the SectionsTooltip component within a group context', ()
|
|||
const tree = mount(<DiscussionRow {...makeProps({ discussion, contextType: "group" })} />)
|
||||
const node = tree.find('SectionsTooltip')
|
||||
notOk(node.exists())
|
||||
|
||||
})
|
||||
|
||||
test('does not render master course lock icon if masterCourseData is not provided', (assert) => {
|
||||
const done = assert.async()
|
||||
const masterCourseData = null
|
||||
const rowRef = (row) => {
|
||||
notOk(row.masterCourseLock)
|
||||
done()
|
||||
}
|
||||
mount(<DiscussionRow {...makeProps({ masterCourseData, rowRef })} />)
|
||||
const tree = mount(<DiscussionRow {...makeProps({ masterCourseData, rowRef })} />)
|
||||
notOk(tree.instance().masterCourseLock)
|
||||
})
|
||||
|
||||
test('renders master course lock icon if masterCourseData is provided', (assert) => {
|
||||
const done = assert.async()
|
||||
const masterCourseData = { isMasterCourse: true, masterCourse: { id: '1' } }
|
||||
const rowRef = (row) => {
|
||||
ok(row.masterCourseLock)
|
||||
done()
|
||||
}
|
||||
mount(<DiscussionRow {...makeProps({ masterCourseData, rowRef })} />)
|
||||
const tree = mount(<DiscussionRow {...makeProps({ masterCourseData, rowRef })} />)
|
||||
ok(tree.instance().masterCourseLock)
|
||||
})
|
||||
|
||||
test('renders drag icon', () => {
|
||||
|
@ -209,23 +249,9 @@ test('renders drag icon', () => {
|
|||
ok(node.exists())
|
||||
})
|
||||
|
||||
test('removes non-text content from discussion message', () => {
|
||||
const messageHtml = `
|
||||
<p>Hello World!</p>
|
||||
<img src="/images/stuff/things.png" />
|
||||
<p>foo bar</p>
|
||||
`
|
||||
const tree = mount(<DiscussionRow {...makeProps({ discussion: { message: messageHtml } })} />)
|
||||
const node = tree.find('.ic-discussion-row__content').getDOMNode()
|
||||
equal(node.childNodes.length, 1)
|
||||
equal(node.childNodes[0].nodeType, 3) // nodeType === 3 is text node type
|
||||
ok(node.textContent.includes('Hello World!'))
|
||||
ok(node.textContent.includes('foo bar'))
|
||||
})
|
||||
|
||||
test('does not render manage menu if not permitted', () => {
|
||||
const tree = mount(<DiscussionRow {...makeProps({ displayManageMenu: false })} />)
|
||||
const node = tree.find('PopoverMenu')
|
||||
const node = tree.find('DiscussionManageMenu')
|
||||
notOk(node.exists())
|
||||
})
|
||||
|
||||
|
@ -235,7 +261,7 @@ test('does not insert the manage menu list if we have not clicked it yet', () =>
|
|||
onMoveDiscussion: ()=>{}
|
||||
})} />)
|
||||
// We still should show the menu thingy itself
|
||||
const menuNode = tree.find('PopoverMenu')
|
||||
const menuNode = tree.find('DiscussionManageMenu')
|
||||
ok(menuNode.exists())
|
||||
// We have to search the whole document because the items in instui
|
||||
// popover menu are appended to the end of the document rather than
|
||||
|
@ -249,7 +275,7 @@ test('manage menu items do appear upon click', () => {
|
|||
displayManageMenu: true,
|
||||
onMoveDiscussion: ()=>{}
|
||||
})} />)
|
||||
const menuNode = tree.find('PopoverMenu')
|
||||
const menuNode = tree.find('DiscussionManageMenu')
|
||||
ok(menuNode.exists())
|
||||
menuNode.find('button').simulate('click')
|
||||
// We have to search the whole document because the items in instui
|
||||
|
@ -264,8 +290,8 @@ test('renders move-to in manage menu if permitted', () => {
|
|||
displayManageMenu: true,
|
||||
onMoveDiscussion: ()=>{}
|
||||
})} />)
|
||||
const courseItemRow = tree.find('CourseItemRow')
|
||||
const allKeys = courseItemRow.props().manageMenuOptions().map((option) => option.key)
|
||||
const manageMenu = tree.find('DiscussionManageMenu')
|
||||
const allKeys = manageMenu.props().menuOptions().map((option) => option.key)
|
||||
equal(allKeys.length, 1)
|
||||
equal(allKeys[0], 'moveTo')
|
||||
})
|
||||
|
@ -275,8 +301,8 @@ test('renders pin item in manage menu if permitted', () => {
|
|||
displayManageMenu: true,
|
||||
displayPinMenuItem: true
|
||||
})} />)
|
||||
const courseItemRow = tree.find('CourseItemRow')
|
||||
const allKeys = courseItemRow.props().manageMenuOptions().map((option) => option.key)
|
||||
const manageMenu = tree.find('DiscussionManageMenu')
|
||||
const allKeys = manageMenu.props().menuOptions().map((option) => option.key)
|
||||
equal(allKeys.length, 1)
|
||||
equal(allKeys[0], 'togglepinned')
|
||||
})
|
||||
|
@ -286,8 +312,8 @@ test('renders duplicate item in manage menu if permitted', () => {
|
|||
displayManageMenu: true,
|
||||
displayDuplicateMenuItem: true
|
||||
})} />)
|
||||
const courseItemRow = tree.find('CourseItemRow')
|
||||
const allKeys = courseItemRow.props().manageMenuOptions().map((option) => option.key)
|
||||
const manageMenu = tree.find('DiscussionManageMenu')
|
||||
const allKeys = manageMenu.props().menuOptions().map((option) => option.key)
|
||||
equal(allKeys.length, 1)
|
||||
equal(allKeys[0], 'duplicate')
|
||||
})
|
||||
|
@ -297,8 +323,8 @@ test('renders delete item in manage menu if permitted', () => {
|
|||
displayManageMenu: true,
|
||||
displayDeleteMenuItem: true
|
||||
})} />)
|
||||
const courseItemRow = tree.find('CourseItemRow')
|
||||
const allKeys = courseItemRow.props().manageMenuOptions().map((option) => option.key)
|
||||
const manageMenu = tree.find('DiscussionManageMenu')
|
||||
const allKeys = manageMenu.props().menuOptions().map((option) => option.key)
|
||||
equal(allKeys.length, 1)
|
||||
equal(allKeys[0], 'delete')
|
||||
})
|
||||
|
@ -308,27 +334,42 @@ test('renders lock item in manage menu if permitted', () => {
|
|||
displayManageMenu: true,
|
||||
displayLockMenuItem: true
|
||||
})} />)
|
||||
const courseItemRow = tree.find('CourseItemRow')
|
||||
const allKeys = courseItemRow.props().manageMenuOptions().map((option) => option.key)
|
||||
const manageMenu = tree.find('DiscussionManageMenu')
|
||||
const allKeys = manageMenu.props().menuOptions().map((option) => option.key)
|
||||
equal(allKeys.length, 1)
|
||||
equal(allKeys[0], 'togglelocked')
|
||||
})
|
||||
|
||||
test('renders mastery paths menu item if permitted', () => {
|
||||
const tree=mount(<DiscussionRow {...makeProps({
|
||||
displayManageMenu: true,
|
||||
discussion: {
|
||||
assignment_id: 2
|
||||
},
|
||||
displayMasteryPathsMenuItem: true
|
||||
})} />)
|
||||
const courseItemRow = tree.find('CourseItemRow')
|
||||
const allKeys = courseItemRow.props().manageMenuOptions().map((option) => option.key)
|
||||
const manageMenu = tree.find('DiscussionManageMenu')
|
||||
const allKeys = manageMenu.props().menuOptions().map((option) => option.key)
|
||||
equal(allKeys.length, 1)
|
||||
equal(allKeys[0], 'masterypaths')
|
||||
})
|
||||
|
||||
test('renders mastery paths link if permitted', () => {
|
||||
const tree=mount(<DiscussionRow {...makeProps({
|
||||
displayManageMenu: true,
|
||||
discussion: {
|
||||
assignment_id: 2
|
||||
},
|
||||
displayMasteryPathsLink: true
|
||||
})} />)
|
||||
const node = tree.find('.discussion-index-mastery-paths-link')
|
||||
ok(node.exists())
|
||||
ok(node.text().includes('Mastery Paths'))
|
||||
})
|
||||
|
||||
test('renders ltiTool menu if there are some', () => {
|
||||
const tree=mount(<DiscussionRow {...makeProps({
|
||||
displayManageMenu: true,
|
||||
discussionTopicMenuTools:[{
|
||||
base_url: "test.com",
|
||||
canvas_icon_class: "icon-lti",
|
||||
|
@ -336,14 +377,15 @@ test('renders ltiTool menu if there are some', () => {
|
|||
title: "discussion_topic_menu Text",
|
||||
}]
|
||||
})} />)
|
||||
const courseItemRow = tree.find('CourseItemRow')
|
||||
const allKeys = courseItemRow.props().manageMenuOptions().map((option) => option.key)
|
||||
const manageMenu = tree.find('DiscussionManageMenu')
|
||||
const allKeys = manageMenu.props().menuOptions().map((option) => option.key)
|
||||
equal(allKeys.length, 1)
|
||||
equal(allKeys[0], 'test.com')
|
||||
})
|
||||
|
||||
test('renders multiple ltiTool menu if there are multiple', () => {
|
||||
const tree=mount(<DiscussionRow {...makeProps({
|
||||
displayManageMenu: true,
|
||||
discussionTopicMenuTools:[
|
||||
{
|
||||
base_url: "test.com",
|
||||
|
@ -359,8 +401,8 @@ test('renders multiple ltiTool menu if there are multiple', () => {
|
|||
}
|
||||
]
|
||||
})} />)
|
||||
const courseItemRow = tree.find('CourseItemRow')
|
||||
const allKeys = courseItemRow.props().manageMenuOptions().map((option) => option.key)
|
||||
const manageMenu = tree.find('DiscussionManageMenu')
|
||||
const allKeys = manageMenu.props().menuOptions().map((option) => option.key)
|
||||
equal(allKeys.length, 2)
|
||||
equal(allKeys[1], 'test2.com')
|
||||
})
|
||||
|
|
|
@ -83,7 +83,7 @@ class DiscussionsIndex
|
|||
end
|
||||
|
||||
def discussion_title(title)
|
||||
f('h3', discussion(title))
|
||||
f('a', discussion(title))
|
||||
end
|
||||
|
||||
def discussion_sections(title)
|
||||
|
|
Loading…
Reference in New Issue