Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Display history as a table and add video actions to history #55

Merged
merged 3 commits into from
Jan 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions app/assets/assets/locales/en/history.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
{
"compare_show": "Compare",
"compare_hide": "Hide comparison",
"compare_hide": "Hide",
"compareAll": "Compare all",
"hideAll": "Hide all",
"when": "When",
"who": "Who",
"changes": "Changes",
"revert": "Revert",
"entity": "Entity",
"moderation": "Moderation",
"action": {
"1": "created",
"2": "removed",
"3": "updated",
"4": "deleted",
"5": "added",
"6": "restored"
"1": "Created",
"2": "Removed",
"3": "Updated",
"4": "Deleted",
"5": "Added",
"6": "Reverted"
},
"this": {
"2": "this speaker",
"3": "this statement"
"1": "Video",
"2": "Speaker",
"3": "Statement"
}
}
27 changes: 18 additions & 9 deletions app/assets/assets/locales/fr/history.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
{
"compare_show": "Comparer",
"compare_hide": "Masquer la comparaison",
"compare_hide": "Masquer",
"compareAll": "Tout comparer",
"hideAll": "Tout masquer",
"when": "Quand",
"who": "Qui",
"changes": "Changements",
"revert": "Restaurer",
"entity": "Entité",
"moderation": "Modération",
"action": {
"1": "a créé",
"2": "a retiré",
"3": "a mis à jour",
"4": "a supprimé",
"5": "a ajouté",
"6": "a restauré"
"1": "Créé",
"2": "Retiré",
"3": "Mis à jour",
"4": "Supprimé",
"5": "Ajouté",
"6": "Restauré"
},
"this": {
"2": "cet intervenant",
"3": "cette affirmation"
"1": "Vidéo",
"2": "Intervenant",
"3": "Affirmation"
}
}
60 changes: 60 additions & 0 deletions app/components/UsersActions/ActionDiff.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Immutable from 'immutable'

import titleCase from '../../lib/title_case'
import { ENTITY_SPEAKER } from '../../constants'
import EntityTitle from './EntityTitle'


class ActionDiff extends PureComponent {
render() {
return (
<div className="action-diff">
{this.props.diff.entrySeq().map(([key, changes]) => (
<div key={ key } className="diff-entry">
<span className="diff-key">
{ titleCase(this.formatChangeKey(key)) }
</span>
<pre className="diff-view">
{ this.renderKeyDiff(key, changes) }
</pre>
</div>
))}
</div>
)
}

renderKeyDiff(key, changes) {
// Value completely changed, show it like prev -> new
if (changes.size === 2 && changes.first().removed && changes.last().added)
return <div>
<span className="removed">{ this.formatChangeValue(changes.first().value, key) }</span>,
<span> -> </span>,
<span className="added">{ this.formatChangeValue(changes.last().value, key) }</span>
</div>
// Generate a real diff
return changes.map((change, idx) => (
<span key={idx}
className={ change.added ? 'added' : change.removed ? 'removed' : '' }>
{ this.formatChangeValue(change.value, key) }
</span>
))
}

formatChangeKey(key) {
return key.replace('_id', '').replace('_', ' ')
}

formatChangeValue(value, key) {
if (key === "speaker_id")
return <EntityTitle entity={ENTITY_SPEAKER} entityId={value} withPrefix={false}/>
return value
}
}

ActionDiff.propTypes = {
diff: PropTypes.instanceOf(Immutable.Map).isRequired
}

export default ActionDiff
148 changes: 148 additions & 0 deletions app/components/UsersActions/ActionsTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import React, { PureComponent } from 'react'
import Immutable from 'immutable'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'

import { TimeSince } from '../Utils/TimeSince'
import UserAppellation from '../Users/UserAppellation'
import { Icon } from '../Utils/Icon'
import ActionDiff from './ActionDiff'
import { generateAllDiffs, generateDiff, hideAllDiffs, hideDiff } from '../../state/user_actions/reducer'
import EntityTitle from './EntityTitle'
import { ACTION_DELETE, ACTION_REMOVE } from '../../constants'
import { revertVideoDebateUserAction } from '../../state/video_debate/history/effects'
import { LoadingFrame } from '../Utils/LoadingFrame'


const ACTIONS_ICONS = [
"plus", // Create
"times", // Remove
"pencil", // Update
"times", // Delete
"plus", // Add
"undo" // Restore
]

@translate(['history', 'main'])
@connect(
state => ({diffs: state.UsersActions.diffs, lastActionsIds: state.UsersActions.lastActionsIds}),
{generateDiff, hideDiff, generateAllDiffs, hideAllDiffs, revertVideoDebateUserAction}
)
class ActionsTable extends PureComponent {
render() {
const availableDiffs = new Immutable.List(this.props.diffs.keys())
return (
<table className="actions-list table">
<thead>{this.renderHeader(availableDiffs)}</thead>
<tbody>{this.renderBody(availableDiffs)}</tbody>
</table>
)
}

// ---- Table header ----

renderHeader = availableDiffs => {
const { t, actions, showRestore, showEntity } = this.props
const isMostlyComparing = availableDiffs.count() / actions.count() > 0.5
return (
<tr>
<th>{t('when')}</th>
<th>{t('who')}</th>
<th>Action</th>
{showEntity && <th>{t('entity')}</th>}
<th>{this.renderCompareAllButton(isMostlyComparing)}</th>
{showRestore && <th>{t('revert')}</th>}
<th>{t('moderation')}</th>
</tr>
)
}

renderCompareAllButton = isMostlyComparing =>
<a className="button" title={this.props.t(isMostlyComparing ? 'hideAll' : 'compareAll')}
onClick={isMostlyComparing ? this.props.hideAllDiffs : this.props.generateAllDiffs}>
{this.props.t('changes')}
</a>

// ---- Table body ----

renderBody = (availableDiffs) => {
if (this.props.isLoading)
return <tr style={{background: 'none'}}><td colSpan={this.getNbCols()}><LoadingFrame/></td></tr>
return this.props.actions.map(a => this.renderAction(availableDiffs, a))
}

renderAction = (availableDiffs, action) => {
if (availableDiffs.includes(action.id))
return [this.renderActionLine(action, true), this.renderDiffLine(action)]
return this.renderActionLine(action)
}

renderActionLine(action, isDiffing=false) {
const {showRestore, showEntity, t, revertVideoDebateUserAction} = this.props
const reversible = showRestore && this.props.lastActionsIds.includes(action.id) &&
([ACTION_DELETE, ACTION_REMOVE].includes(action.type))

return (
<tr key={action.id}>
<td><TimeSince time={ action.time }/></td>
<td><UserAppellation user={action.user}/></td>
<td>{this.renderActionIcon(action.type)}<strong>{ t(`action.${action.type}`) }</strong></td>
{showEntity && <td><EntityTitle entity={action.entity} entityId={action.entity_id}/></td>}
<td>
<a className='button' onClick={() => this.toggleDiff(action, isDiffing)}>
<Icon size="small" name="indent"/>
<span>{ t(isDiffing ? 'compare_hide' : 'compare_show') } </span>
</a>
</td>
{showRestore && <td>{reversible &&
<a className="button" onClick={() => revertVideoDebateUserAction(action)}>
<Icon size="small" name="undo"/>
<span>{t('revert')}</span>
</a>
}</td>}
<td>
<a className="is-disabled button">
<Icon size="small" name="check"/>
<span>{t('main:actions.approve')}</span>
</a>&nbsp;&nbsp;
<a key="flag" className="is-disabled button">
<Icon size="small" name="flag"/>
<span>{t('main:actions.flag')}</span>
</a>
</td>
</tr>
)
}

renderDiffLine = action =>
<tr key={`${action.id}-diff`}>
<td colSpan={this.getNbCols()} style={{padding: 0}}>
<ActionDiff action={action} diff={this.props.diffs.get(action.id)}/>
</td>
</tr>

renderActionIcon = type =>
type <= ACTIONS_ICONS.length ? <Icon name={ACTIONS_ICONS[type - 1]} size="mini"/> : null

toggleDiff = (action, isDiffing) =>
isDiffing ? this.props.hideDiff(action) : this.props.generateDiff(action)

getNbCols = () =>
7 - !this.props.showRestore - !this.props.showEntity
}

ActionsTable.defaultProps = {
isLoading: false,
showRestore: true,
showEntity: true
}

ActionsTable.propTypes = {
actions: PropTypes.instanceOf(Immutable.List).isRequired,
isLoading: PropTypes.bool,
showRestore: PropTypes.bool,
showEntity: PropTypes.bool
}

export default ActionsTable
37 changes: 37 additions & 0 deletions app/components/UsersActions/EntityTitle.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { Link } from 'react-router'

import { ENTITY_SPEAKER, ENTITY_VIDEO } from '../../constants'


@translate('history')
class EntityTitle extends PureComponent {
render() {
const {t, entity, entityId, withPrefix} = this.props
let label = null

if (entity === ENTITY_VIDEO)
return t(`this.${entity}`)
if (withPrefix)
label = t(`this.${entity}`) + ` #${entityId}`
else
label = `#${entityId}`
if (entity === ENTITY_SPEAKER)
return <Link to={`/s/${entityId}`}>{label}</Link>
return label
}
}

EntityTitle.propTypes = {
entity: PropTypes.number.isRequired,
entityId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
withPrefix: PropTypes.bool,
}

EntityTitle.defaultProps = {
withPrefix: true
}

export default EntityTitle
4 changes: 2 additions & 2 deletions app/components/VideoDebate/ColumnDebate.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react"
import { connect } from "react-redux"
import { Trans, translate } from 'react-i18next'

import History from "./VideoDebateHistory"
import VideoDebateHistory from "./VideoDebateHistory"
import ActionBubbleMenu from './ActionBubbleMenu'
import StatementsList from '../Statements/StatementsList'
import { LoadingFrame } from '../Utils/LoadingFrame'
Expand All @@ -28,7 +28,7 @@ export class ColumnDebate extends React.PureComponent {
const { isLoading, view, videoId, hasStatements } = this.props

if (view === 'history')
return <History videoId={ videoId }/>
return <VideoDebateHistory videoId={ videoId }/>
else if (view === 'debate') {
if (isLoading)
return <LoadingFrame title={this.props.t('loading.statements')}/>
Expand Down
Loading