Skip to content

Commit

Permalink
Merge pull request #55 from CaptainFact/improvement/history
Browse files Browse the repository at this point in the history
Display history as a table and add video actions to history
  • Loading branch information
Betree committed Jan 4, 2018
2 parents 8cead75 + 9e423c2 commit b51c922
Show file tree
Hide file tree
Showing 16 changed files with 439 additions and 209 deletions.
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

0 comments on commit b51c922

Please sign in to comment.