diff --git a/app/jsx/discussions/actions.js b/app/jsx/discussions/actions.js index e21766e8d22..7f1767c6ef7 100644 --- a/app/jsx/discussions/actions.js +++ b/app/jsx/discussions/actions.js @@ -31,6 +31,9 @@ const discussionActions = createPaginationActions('discussions', apiClient.getDi const types = [ ...discussionActions.actionTypes, + 'DRAG_AND_DROP_START', + 'DRAG_AND_DROP_SUCCESS', + 'DRAG_AND_DROP_FAIL', 'UPDATE_DISCUSSIONS_SEARCH', 'TOGGLE_MODAL_OPEN', 'TOGGLE_SUBSCRIBE_START', @@ -86,6 +89,8 @@ const defaultFailMessage = I18n.t('Updating discussion failed') // focusOn must be one of 'title' or 'manageMenu' (or can be left unspecified) // If set to a value, it will cause focus to end up on the title or manage menu // of the updated discussion. + +// TODO change this to the onSuccess paradigm. Much easier actions.updateDiscussion = function(discussion, updatedFields, { successMessage, failMessage }, focusOn) { return (dispatch, getState) => { const discussionCopy = copyAndUpdateDiscussion(discussion, updatedFields, focusOn) @@ -109,6 +114,57 @@ actions.updateDiscussion = function(discussion, updatedFields, { successMessage, } } + +// We need to assume success here, so that when we drag and drop something +// it does not snap back to its current location and then move to the +// correct location after the API call succeeds. This is a bit more complex +// as we could be making two API calls here as well (pinning a discussion +// and setting the order of the pin). Start by updating the store with +// this information, then rollback based on if either of the API calls +// failed. +actions.handleDrop = function(discussion, updatedFields, order) { + return (dispatch, getState) => { + const originalOrder = order ? getState().pinnedDiscussions.map(d => d.id) : undefined + const discussionCopy = copyAndUpdateDiscussion(discussion, updatedFields) + dispatch(actions.dragAndDropStart({discussion: discussionCopy, order})) + apiClient.updateDiscussion(getState(), discussion, updatedFields) + .then(() => { + // Only need to make this API call if reordering the pinned discussions + const promise = (discussionCopy.pinned && order !== undefined) + ? apiClient.reorderPinnedDiscussions(getState(), order) + : new Promise(resolve => resolve()) + + promise + .then(() => { + dispatch(actions.dragAndDropSuccess()) + }) + .catch(err => { + // container state has already been updated, so we are only reverting + // the pinned order here. By default, if this is a discussion that + // that just got moved to the pinned container, we will move it to + // the bottom of the pinned discussions on error + if (discussionCopy.pinned) { originalOrder.push(discussionCopy.id) } + dispatch(actions.dragAndDropFail({ + message: I18n.t('Failed to update discussion'), + discussion: discussionCopy, + order: originalOrder, + err, + })) + }) + }) + .catch(err => { + // reset order and discussion back to original state + dispatch(actions.dragAndDropFail({ + message: I18n.t('Failed to update discussion'), + discussion, + order: originalOrder, + err, + })) + }) + } +} + + actions.searchDiscussions = function searchDiscussions ({ searchTerm, filter }) { return (dispatch, getState) => { dispatch(actions.updateDiscussionsSearch({ searchTerm, filter })) diff --git a/app/jsx/discussions/apiClient.js b/app/jsx/discussions/apiClient.js index 32b39b44a79..0dbb615e4e0 100644 --- a/app/jsx/discussions/apiClient.js +++ b/app/jsx/discussions/apiClient.js @@ -83,3 +83,9 @@ export function saveUserSettings ({currentUserId}, settings) { export function duplicateDiscussion ({ contextType, contextId }, discussionId) { return axios.post(`/api/v1/${contextType}s/${contextId}/discussion_topics/${discussionId}/duplicate`) } + +export function reorderPinnedDiscussions ({ contextType, contextId }, order) { + const postData = { order: order.join(',') } + const url = `/api/v1/${contextType}s/${contextId}/discussion_topics/reorder` + return axios.post(url, postData) +} diff --git a/app/jsx/discussions/components/DiscussionContainer.js b/app/jsx/discussions/components/DiscussionContainer.js index 03416509645..110434b592e 100644 --- a/app/jsx/discussions/components/DiscussionContainer.js +++ b/app/jsx/discussions/components/DiscussionContainer.js @@ -23,49 +23,31 @@ import I18n from 'i18n!discussions_v2' import ToggleDetails from '@instructure/ui-core/lib/components/ToggleDetails' import Text from '@instructure/ui-core/lib/components/Text' +import update from 'immutability-helper' import DiscussionRow, { DraggableDiscussionRow } from '../../shared/components/DiscussionRow' import { discussionList } from '../../shared/proptypes/discussion' import masterCourseDataShape from '../../shared/proptypes/masterCourseData' import propTypes from '../propTypes' -// We need to look at the previous state of a discussion as well as where it is -// trying to be drag and dropped into in order to create a decent screenreader -// success and fail message -const generateDragAndDropMessages = (props, discussion) => { - if (props.pinned) { - return { - successMessage: I18n.t('Discussion pinned successfully'), - failMessage: I18n.t('Failed to pin discussion'), - } - } else if (discussion.pinned) { - return { - successMessage: I18n.t('Discussion unpinned successfully'), - failMessage: I18n.t('Failed to unpin discussion'), - } - } else if (props.closedState) { - return { - successMessage: I18n.t('Discussion opened for comments successfully'), - failMessage: I18n.t('Failed to close discussion for comments'), - } - } else { - return { - successMessage: I18n.t('Discussion closed for comments successfully'), - failMessage: I18n.t('Failed to open discussion for comments'), - } - } -} // Handle drag and drop on a discussion. The props passed in tell us how we // should update the discussion if something is dragged into this container const discussionTarget = { - drop(props, monitor) { + drop(props, monitor, component) { const discussion = monitor.getItem() const updateFields = {} if (props.closedState !== undefined) updateFields.locked = props.closedState if (props.pinned !== undefined) updateFields.pinned = props.pinned - const flashMessages = generateDragAndDropMessages(props, discussion) - props.updateDiscussion(discussion, updateFields, flashMessages) + + // We currently cannot drag an item from a different container to a specific + // position in the pinned container, thus we only need to set the order when + // rearranging items in the pinned container, not when dragging a locked or + // unpinned discussion to the pinned container. + const order = (props.pinned && discussion.pinned) + ? component.state.discussions.map(d => d.id) + : undefined + props.handleDrop(discussion, updateFields, order) }, } @@ -77,12 +59,13 @@ export default class DiscussionsContainer extends Component { title: string.isRequired, toggleSubscribe: func.isRequired, updateDiscussion: func.isRequired, + handleDrop: func, // eslint-disable-line duplicateDiscussion: func.isRequired, cleanDiscussionFocus: func.isRequired, pinned: bool, - closedState: bool, + closedState: bool, // eslint-disable-line connectDropTarget: func, - roles: arrayOf(string), + roles: arrayOf(string), // eslint-disable-line renderContainerBackground: func.isRequired, onMoveDiscussion: func, deleteDiscussion: func, @@ -95,14 +78,23 @@ export default class DiscussionsContainer extends Component { closedState: undefined, roles: ['user', 'student'], onMoveDiscussion: null, - deleteDiscussion: null + deleteDiscussion: null, + handleDrop: undefined, } - componentWillReceiveProps(newProps) { + constructor(props) { + super(props) + this.moveCard = this.moveCard.bind(this) + this.state = { + discussions: props.discussions, + } + } + + componentWillReceiveProps(props) { if((this.props.discussions.length >= 1 - && newProps.discussions.length === 0) - || (newProps.discussions[0] - && newProps.discussions[0].focusOn === "toggleButton")) { + && props.discussions.length === 0) + || (props.discussions[0] + && props.discussions[0].focusOn === "toggleButton")) { if(this.toggleBtn) { setTimeout(() => { this.toggleBtn.focus() @@ -110,18 +102,36 @@ export default class DiscussionsContainer extends Component { }); } } + + if(this.props.discussions !== props.discussions) { + this.setState({ + discussions: props.discussions, + }) + } } wrapperToggleRef = (c) => { this.toggleBtn = c && c.querySelector('button') } - renderDiscussions () { - return this.props.discussions.reduce((accumlator, discussion) => { - if (discussion.filtered) { - return accumlator - } + moveCard(dragIndex, hoverIndex) { + const { discussions } = this.state + const dragDiscussion = discussions[dragIndex] + if (!dragDiscussion) { + return + } + const newDiscussions = update(this.state, { + discussions: { + $splice: [[dragIndex, 1], [hoverIndex, 0, dragDiscussion]], + }, + }) + newDiscussions.discussions = newDiscussions.discussions.map((discussion, index) => ({...discussion, sortableId: index})) + this.setState({discussions: newDiscussions.discussions}) + } + renderDiscussions () { + return this.state.discussions.reduce((accumlator, discussion) => { + if (discussion.filtered) { return accumlator } const row = this.props.permissions.moderate ? : pinnedDiscussionBackground({ - permissions: this.props.permissions + permissions: this.props.permissions, }) } /> @@ -224,13 +226,14 @@ export default class DiscussionsIndex extends Component { document.getElementById('application')} - />)} - + />)} ) } @@ -313,6 +316,7 @@ const connectActions = dispatch => 'getDiscussions', 'toggleSubscriptionState', 'updateDiscussion', + 'handleDrop', 'duplicateDiscussion', 'cleanDiscussionFocus', 'arrangePinnedDiscussions', diff --git a/app/jsx/discussions/reducers/closedForCommentsDiscussionReducer.js b/app/jsx/discussions/reducers/closedForCommentsDiscussionReducer.js index 2e6b77d9b98..b145b173851 100644 --- a/app/jsx/discussions/reducers/closedForCommentsDiscussionReducer.js +++ b/app/jsx/discussions/reducers/closedForCommentsDiscussionReducer.js @@ -56,6 +56,12 @@ const reducerMap = { [actionTypes.UPDATE_DISCUSSION_FAIL]: (state, action) => ( copyAndUpdateDiscussionState(state, action.payload.discussion) ), + [actionTypes.DRAG_AND_DROP_START]: (state, action) => ( + copyAndUpdateDiscussionState(state, action.payload.discussion) + ), + [actionTypes.DRAG_AND_DROP_FAIL]: (state, action) => ( + copyAndUpdateDiscussionState(state, action.payload.discussion) + ), } Object.assign(reducerMap, subscriptionReducerMap, duplicationReducerMap, diff --git a/app/jsx/discussions/reducers/deleteReducerMap.js b/app/jsx/discussions/reducers/deleteReducerMap.js index 37818925827..197575f058e 100644 --- a/app/jsx/discussions/reducers/deleteReducerMap.js +++ b/app/jsx/discussions/reducers/deleteReducerMap.js @@ -16,6 +16,7 @@ * with this program. If not, see . */ import { actionTypes } from '../actions' +import { setSortableId } from '../utils' const deleteReducerMap = { [actionTypes.DELETE_DISCUSSION_SUCCESS]: (state, action) => { @@ -24,20 +25,20 @@ const deleteReducerMap = { )) const newState= state.slice() if (oldIndex < 0) { - return newState; + return newState } if(oldIndex === 0) { newState.splice(oldIndex, 1) if(newState.length) { newState[0] = { ...newState[0], focusOn: "toggleButton" } } - return newState; + return setSortableId(newState) } else { const newFocusIndex = oldIndex - 1; if(newState[newFocusIndex]) { newState[newFocusIndex] = { ...newState[newFocusIndex], focusOn: "manageMenu" } newState.splice(oldIndex, 1) - return newState + return setSortableId(newState) } else { // There is no discussions left return [] } diff --git a/app/jsx/discussions/reducers/duplicationReducerMap.js b/app/jsx/discussions/reducers/duplicationReducerMap.js index fb583f938b8..438491510f0 100644 --- a/app/jsx/discussions/reducers/duplicationReducerMap.js +++ b/app/jsx/discussions/reducers/duplicationReducerMap.js @@ -16,6 +16,7 @@ * with this program. If not, see . */ import { actionTypes } from '../actions' +import { setSortableId } from '../utils' function updatePositions(discussions, updatedPositions) { return discussions.map( (discussion) => { @@ -47,7 +48,8 @@ const duplicationReducerMap = { : remainingOriginalDiscussions delete newDiscussion.new_positions - return newStateBeginning.concat(newStateEnd) + const unsortedDiscussions = newStateBeginning.concat(newStateEnd) + return setSortableId(unsortedDiscussions) } else { // The original discussion wasn't in this container, so the state should // not be changed. diff --git a/app/jsx/discussions/reducers/pinnedDiscussionReducer.js b/app/jsx/discussions/reducers/pinnedDiscussionReducer.js index 0ee76971bf8..8aa606f072d 100644 --- a/app/jsx/discussions/reducers/pinnedDiscussionReducer.js +++ b/app/jsx/discussions/reducers/pinnedDiscussionReducer.js @@ -24,6 +24,7 @@ import duplicationReducerMap from './duplicationReducerMap' import deleteReducerMap from './deleteReducerMap' import cleanDiscussionFocusReducerMap from './cleanDiscussionFocusReducerMap' import searchReducerMap from './searchReducerMap' +import { setSortableId } from '../utils' // We need to not change the ordering of the diccussions here, ie we can't // use the same .sort that we use in the GET_DISCUSSIONS_SUCCESS. This is @@ -44,20 +45,14 @@ function copyAndUpdateDiscussionState(oldState, updatedDiscussion) { return newState } +function orderPinnedDiscussions(state, order) { + const discussions = order ? order.map(id => state.find(discussion => discussion.id === id)) : state + return setSortableId(discussions) +} + const reducerMap = { - [actionTypes.ARRANGE_PINNED_DISCUSSIONS]: (state, action) => { - if (!action.payload.order) { - return state - } - const oldDiscussionList = state.slice() - const newPinnedDiscussions = action.payload.order.map(id => { - const currentDiscussion = oldDiscussionList.find( - discussion => discussion.id === id - ) - return currentDiscussion - }) - return newPinnedDiscussions - }, + [actionTypes.ARRANGE_PINNED_DISCUSSIONS]: (state, action) => + orderPinnedDiscussions(state, action.payload.order), [actionTypes.GET_DISCUSSIONS_SUCCESS]: (state, action) => { const discussions = action.payload.data || [] const pinnedDiscussions = discussions.reduce((accumlator, discussion) => { @@ -66,12 +61,20 @@ const reducerMap = { } return accumlator }, []) - return orderBy(pinnedDiscussions, ['position'], ['asc']) + return setSortableId(orderBy(pinnedDiscussions, ['position'], ['asc'])) }, [actionTypes.UPDATE_DISCUSSION_START]: (state, action) => copyAndUpdateDiscussionState(state, action.payload.discussion), [actionTypes.UPDATE_DISCUSSION_FAIL]: (state, action) => - copyAndUpdateDiscussionState(state, action.payload.discussion) + copyAndUpdateDiscussionState(state, action.payload.discussion), + [actionTypes.DRAG_AND_DROP_START]: (state, action) => { + const updatedState = copyAndUpdateDiscussionState(state, action.payload.discussion) + return orderPinnedDiscussions(updatedState, action.payload.order) + }, + [actionTypes.DRAG_AND_DROP_FAIL]: (state, action) => { + const updatedState = copyAndUpdateDiscussionState(state, action.payload.discussion) + return orderPinnedDiscussions(updatedState, action.payload.order) + } } Object.assign(reducerMap, subscriptionReducerMap, duplicationReducerMap, cleanDiscussionFocusReducerMap, searchReducerMap, deleteReducerMap) diff --git a/app/jsx/discussions/reducers/unpinnedDiscussionReducer.js b/app/jsx/discussions/reducers/unpinnedDiscussionReducer.js index df827eb7986..22b1a2bb373 100644 --- a/app/jsx/discussions/reducers/unpinnedDiscussionReducer.js +++ b/app/jsx/discussions/reducers/unpinnedDiscussionReducer.js @@ -56,6 +56,12 @@ const reducerMap = { [actionTypes.UPDATE_DISCUSSION_FAIL]: (state, action) => ( copyAndUpdateDiscussionState(state, action.payload.discussion) ), + [actionTypes.DRAG_AND_DROP_START]: (state, action) => ( + copyAndUpdateDiscussionState(state, action.payload.discussion) + ), + [actionTypes.DRAG_AND_DROP_FAIL]: (state, action) => ( + copyAndUpdateDiscussionState(state, action.payload.discussion) + ), } Object.assign(reducerMap, subscriptionReducerMap, duplicationReducerMap, diff --git a/app/jsx/discussions/utils.js b/app/jsx/discussions/utils.js index f33c149bf66..365776525d2 100644 --- a/app/jsx/discussions/utils.js +++ b/app/jsx/discussions/utils.js @@ -19,3 +19,14 @@ export function reorderDiscussionsURL ({ contextType, contextId }) { return `/api/v1/${contextType}s/${contextId}/discussion_topics/reorder` } + +export function setSortableId(discussions) { + let pinnedDiscussion = false + for( let i = 0; i < discussions.length; i++) { + if(discussions[i].pinned) { + pinnedDiscussion = true + break; + } + } + return !pinnedDiscussion ? discussions : discussions.map((discussion, index) => ({...discussion, sortableId: index})) +} diff --git a/app/jsx/shared/components/CourseItemRow.js b/app/jsx/shared/components/CourseItemRow.js index 3ee4640819d..dafeb7114fe 100644 --- a/app/jsx/shared/components/CourseItemRow.js +++ b/app/jsx/shared/components/CourseItemRow.js @@ -39,7 +39,7 @@ import masterCourseDataShape from '../proptypes/masterCourseData' export default class CourseItemRow extends Component { static propTypes = { - actionsContent: node, + actionsContent: arrayOf(node), metaContent: node, masterCourse: shape({ courseData: masterCourseDataShape, @@ -48,7 +48,9 @@ export default class CourseItemRow extends Component { author: authorShape, title: string.isRequired, body: node, + isDragging: bool, connectDragSource: func, + connectDropTarget: func, id: string, className: string, itemUrl: string, @@ -82,6 +84,7 @@ export default class CourseItemRow extends Component { }, id: null, className: '', + isDragging: false, itemUrl: null, selectable: false, draggable: false, @@ -89,7 +92,8 @@ export default class CourseItemRow extends Component { isRead: true, icon: null, showAvatar: false, - connectDragSource: null, + connectDragSource: (component) => component, + connectDropTarget: (component) => component, onSelectedChanged () {}, showManageMenu: false, manageMenuOptions: [], @@ -105,10 +109,6 @@ export default class CourseItemRow extends Component { isSelected: this.props.defaultSelected, } - componentWillUnmount () { - this.unmountMasterCourseLock() - } - componentDidMount () { this.onFocusManage(this.props) } @@ -133,6 +133,10 @@ export default class CourseItemRow extends Component { } } + componentWillUnmount () { + this.unmountMasterCourseLock() + } + onSelectChanged = (e) => { this.setState({ isSelected: e.target.checked }, () => { this.props.onSelectedChanged({ selected: this.state.isSelected, id: this.props.id }) @@ -178,15 +182,14 @@ export default class CourseItemRow extends Component { render () { const classes = cx('ic-item-row') return ( -
+ this.props.connectDropTarget(this.props.connectDragSource( +
{(this.props.draggable && this.props.connectDragSource &&
- {this.props.connectDragSource( - - - - - , {dropEffect: 'copy'}) - } + + + + +
)} { !this.props.isRead ? ( @@ -242,7 +245,7 @@ export default class CourseItemRow extends Component { {this.props.metaContent}
- + , {dropEffect: 'copy'})) ) } } diff --git a/app/jsx/shared/components/DiscussionRow.js b/app/jsx/shared/components/DiscussionRow.js index 9c12ae687b0..bf367cab40a 100644 --- a/app/jsx/shared/components/DiscussionRow.js +++ b/app/jsx/shared/components/DiscussionRow.js @@ -17,12 +17,13 @@ */ import I18n from 'i18n!discussion_row' -import React from 'react' +import React, { Component } from 'react' import { func, bool } from 'prop-types' import $ from 'jquery' import 'jquery.instructure_date_and_time' -import { DragSource } from 'react-dnd'; +import { DragSource, DropTarget } from 'react-dnd'; +import { findDOMNode } from 'react-dom' import Container from '@instructure/ui-core/lib/components/Container' import Badge from '@instructure/ui-core/lib/components/Badge' import Text from '@instructure/ui-core/lib/components/Text' @@ -43,9 +44,11 @@ import IconPinLine from 'instructure-icons/lib/Line/IconPinLine' import IconReply from 'instructure-icons/lib/Line/IconReplyLine' import DiscussionModel from 'compiled/models/DiscussionTopic' +import compose from '../helpers/compose' import SectionsTooltip from '../SectionsTooltip' import CourseItemRow from './CourseItemRow' import UnreadBadge from './UnreadBadge' + import ToggleIcon from './ToggleIcon' import discussionShape from '../proptypes/discussion' import masterCourseDataShape from '../proptypes/masterCourseData' @@ -71,80 +74,144 @@ const discussionTarget = { }, } -export default function DiscussionRow ({ discussion, masterCourseData, rowRef, onSelectedChanged, - connectDragSource, connectDragPreview, draggable, - onToggleSubscribe, updateDiscussion, canManage, canPublish, - duplicateDiscussion, cleanDiscussionFocus, onMoveDiscussion, - deleteDiscussion }) { - const makePinSuccessFailMessages = () => { - const successMessage = discussion.pinned ? - I18n.t('Unpin of discussion %{title} succeeded', { title: discussion.title }) : - I18n.t('Pin of discussion %{title} succeeded', { title: discussion.title }) - const failMessage = discussion.pinned ? - I18n.t('Unpin of discussion %{title} failed', { title: discussion.title }) : - I18n.t('Pin of discussion %{title} failed', { title: discussion.title }) - return { successMessage, failMessage } +const otherTarget = { + hover(props, monitor, component) { + const dragIndex = monitor.getItem().sortableId + const hoverIndex = props.discussion.sortableId + if (dragIndex === undefined || hoverIndex === undefined) { + return + } + if (dragIndex === hoverIndex) { + return + } + const hoverBoundingRect = findDOMNode(component).getBoundingClientRect() // eslint-disable-line + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 + const clientOffset = monitor.getClientOffset() + const hoverClientY = clientOffset.y - hoverBoundingRect.top + // Only perform the move when the mouse has crossed half of the items height + // When dragging downwards, only move when the cursor is below 50% + // When dragging upwards, only move when the cursor is above 50% + if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { + return + } + if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { + return + } + props.moveCard(dragIndex, hoverIndex) + monitor.getItem().sortableId = hoverIndex // eslint-disable-line + }, +} + +export default class DiscussionRow extends Component { + static propTypes = { + discussion: discussionShape.isRequired, + masterCourseData: masterCourseDataShape, + rowRef: func, + moveCard: func, // eslint-disable-line + deleteDiscussion: func.isRequired, + onSelectedChanged: func, + connectDragSource: func, + connectDragPreview: func, + connectDropTarget: func, + isDragging: bool, + draggable: bool, + onToggleSubscribe: func.isRequired, + canManage: bool.isRequired, + duplicateDiscussion: func.isRequired, + cleanDiscussionFocus: func.isRequired, + updateDiscussion: func.isRequired, + canPublish: bool.isRequired, + onMoveDiscussion: func, } - const onManageDiscussion = (e, { action, id }) => { + static defaultProps = { + connectDragPreview (component) {return component}, + connectDragSource (component) {return component}, + connectDropTarget (component) {return component}, + draggable: false, + isDragging: false, + masterCourseData: null, + moveCard: () => {}, + onMoveDiscussion: null, + onSelectedChanged () {}, + rowRef () {}, + } + + onManageDiscussion = (e, { action, id }) => { switch (action) { case 'duplicate': - duplicateDiscussion(id) + this.props.duplicateDiscussion(id) break case 'moveTo': - onMoveDiscussion({ id, title: discussion.title }) + this.props.onMoveDiscussion({ id, title: this.props.discussion.title }) break case 'togglepinned': - updateDiscussion(discussion, { pinned: !discussion.pinned }, - makePinSuccessFailMessages(discussion), 'manageMenu') + this.props.updateDiscussion(this.props.discussion, { pinned: !this.props.discussion.pinned }, + this.makePinSuccessFailMessages(this.props.discussion), 'manageMenu') break case 'delete': - deleteDiscussion(discussion) + this.props.deleteDiscussion(this.props.discussion) break case 'togglelocked': - updateDiscussion(discussion, { locked: !discussion.locked }, - makePinSuccessFailMessages(discussion), 'manageMenu') + this.props.updateDiscussion(this.props.discussion, { locked: !this.props.discussion.locked }, + this.makePinSuccessFailMessages(this.props.discussion), 'manageMenu') break default: throw new Error(I18n.t('Unknown manage discussion action encountered')) } } - const timestamp = makeTimestamp(discussion) - const readCount = discussion.discussion_subentry_count > 0 + makePinSuccessFailMessages = () => { + const successMessage = this.props.discussion.pinned ? + I18n.t('Unpin of discussion %{title} succeeded', { title: this.props.discussion.title }) : + I18n.t('Pin of discussion %{title} succeeded', { title: this.props.discussion.title }) + const failMessage = this.props.discussion.pinned ? + I18n.t('Unpin of discussion %{title} failed', { title: this.props.discussion.title }) : + I18n.t('Pin of discussion %{title} failed', { title: this.props.discussion.title }) + return { successMessage, failMessage } + } + + readCount = () => { + const readCount = this.props.discussion.discussion_subentry_count > 0 ? ( ) : null - const subscribeButton = ( + return readCount + } + + subscribeButton = () => ( } - OffIcon={} - onToggleOn={() => onToggleSubscribe(discussion)} - onToggleOff={() => onToggleSubscribe(discussion)} - disabled={discussion.subscription_hold !== undefined} + toggled={this.props.discussion.subscribed} + OnIcon={} + OffIcon={} + onToggleOn={() => this.props.onToggleSubscribe(this.props.discussion)} + onToggleOff={() => this.props.onToggleSubscribe(this.props.discussion)} + disabled={this.props.discussion.subscription_hold !== undefined} className="subscribe-button" /> ) - const publishButton = canPublish + + publishButton = () => ( + this.props.canPublish ? (} - OffIcon={} - onToggleOn={() => updateDiscussion(discussion, {published: true}, {})} - onToggleOff={() => updateDiscussion(discussion, {published: false}, {})} + toggled={this.props.discussion.published} + OnIcon={} + OffIcon={} + onToggleOn={() => this.props.updateDiscussion(this.props.discussion, {published: true}, {})} + onToggleOff={() => this.props.updateDiscussion(this.props.discussion, {published: false}, {})} className="publish-button" />) : null + ) - const pinMenuItemDisplay = () =>{ - if (discussion.pinned) { + pinMenuItemDisplay = () => { + if (this.props.discussion.pinned) { return (