@ -0,0 +1,64 @@ | |||||
import api from '../api'; | |||||
export const REPORT_INIT = 'REPORT_INIT'; | |||||
export const REPORT_CANCEL = 'REPORT_CANCEL'; | |||||
export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST'; | |||||
export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS'; | |||||
export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; | |||||
export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE'; | |||||
export function initReport(account, status) { | |||||
return { | |||||
type: REPORT_INIT, | |||||
account, | |||||
status | |||||
}; | |||||
}; | |||||
export function cancelReport() { | |||||
return { | |||||
type: REPORT_CANCEL | |||||
}; | |||||
}; | |||||
export function toggleStatusReport(statusId, checked) { | |||||
return { | |||||
type: REPORT_STATUS_TOGGLE, | |||||
statusId, | |||||
checked, | |||||
}; | |||||
}; | |||||
export function submitReport() { | |||||
return (dispatch, getState) => { | |||||
dispatch(submitReportRequest()); | |||||
api(getState).post('/api/v1/reports', { | |||||
account_id: getState().getIn(['reports', 'new', 'account_id']), | |||||
status_ids: getState().getIn(['reports', 'new', 'status_ids']), | |||||
comment: getState().getIn(['reports', 'new', 'comment']) | |||||
}).then(response => dispatch(submitReportSuccess(response.data))).catch(error => dispatch(submitReportFail(error))); | |||||
}; | |||||
}; | |||||
export function submitReportRequest() { | |||||
return { | |||||
type: REPORT_SUBMIT_REQUEST | |||||
}; | |||||
}; | |||||
export function submitReportSuccess(report) { | |||||
return { | |||||
type: REPORT_SUBMIT_SUCCESS, | |||||
report | |||||
}; | |||||
}; | |||||
export function submitReportFail(error) { | |||||
return { | |||||
type: REPORT_SUBMIT_FAIL, | |||||
error | |||||
}; | |||||
}; |
@ -0,0 +1,38 @@ | |||||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||||
import emojify from '../../../emoji'; | |||||
import Toggle from 'react-toggle'; | |||||
const StatusCheckBox = React.createClass({ | |||||
propTypes: { | |||||
status: ImmutablePropTypes.map.isRequired, | |||||
checked: React.PropTypes.bool, | |||||
onToggle: React.PropTypes.func.isRequired, | |||||
disabled: React.PropTypes.bool | |||||
}, | |||||
mixins: [PureRenderMixin], | |||||
render () { | |||||
const { status, checked, onToggle, disabled } = this.props; | |||||
const content = { __html: emojify(status.get('content')) }; | |||||
return ( | |||||
<div className='status-check-box' style={{ display: 'flex' }}> | |||||
<div | |||||
className='status__content' | |||||
style={{ flex: '1 1 auto', padding: '10px' }} | |||||
dangerouslySetInnerHTML={content} | |||||
/> | |||||
<div style={{ flex: '0 0 auto', padding: '10px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}> | |||||
<Toggle checked={checked} onChange={onToggle} disabled={disabled} /> | |||||
</div> | |||||
</div> | |||||
); | |||||
} | |||||
}); | |||||
export default StatusCheckBox; |
@ -0,0 +1,19 @@ | |||||
import { connect } from 'react-redux'; | |||||
import StatusCheckBox from '../components/status_check_box'; | |||||
import { toggleStatusReport } from '../../../actions/reports'; | |||||
import Immutable from 'immutable'; | |||||
const mapStateToProps = (state, { id }) => ({ | |||||
status: state.getIn(['statuses', id]), | |||||
checked: state.getIn(['reports', 'new', 'status_ids'], Immutable.Set()).includes(id) | |||||
}); | |||||
const mapDispatchToProps = (dispatch, { id }) => ({ | |||||
onToggle (e) { | |||||
dispatch(toggleStatusReport(id, e.target.checked)); | |||||
} | |||||
}); | |||||
export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox); |
@ -0,0 +1,130 @@ | |||||
import { connect } from 'react-redux'; | |||||
import { cancelReport, changeReportComment, submitReport } from '../../actions/reports'; | |||||
import { fetchAccountTimeline } from '../../actions/accounts'; | |||||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||||
import Column from '../ui/components/column'; | |||||
import Button from '../../components/button'; | |||||
import { makeGetAccount } from '../../selectors'; | |||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; | |||||
import StatusCheckBox from './containers/status_check_box_container'; | |||||
import Immutable from 'immutable'; | |||||
import ColumnBackButtonSlim from '../../components/column_back_button_slim'; | |||||
const messages = defineMessages({ | |||||
heading: { id: 'report.heading', defaultMessage: 'New report' }, | |||||
placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' }, | |||||
submit: { id: 'report.submit', defaultMessage: 'Submit' } | |||||
}); | |||||
const makeMapStateToProps = () => { | |||||
const getAccount = makeGetAccount(); | |||||
const mapStateToProps = state => { | |||||
const accountId = state.getIn(['reports', 'new', 'account_id']); | |||||
return { | |||||
isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), | |||||
account: getAccount(state, accountId), | |||||
comment: state.getIn(['reports', 'new', 'comment']), | |||||
statusIds: state.getIn(['timelines', 'accounts_timelines', accountId, 'items'], Immutable.List()) | |||||
}; | |||||
}; | |||||
return mapStateToProps; | |||||
}; | |||||
const textareaStyle = { | |||||
marginBottom: '10px' | |||||
}; | |||||
const Report = React.createClass({ | |||||
contextTypes: { | |||||
router: React.PropTypes.object | |||||
}, | |||||
propTypes: { | |||||
isSubmitting: React.PropTypes.bool, | |||||
account: ImmutablePropTypes.map, | |||||
statusIds: ImmutablePropTypes.list.isRequired, | |||||
comment: React.PropTypes.string.isRequired, | |||||
dispatch: React.PropTypes.func.isRequired, | |||||
intl: React.PropTypes.object.isRequired | |||||
}, | |||||
mixins: [PureRenderMixin], | |||||
componentWillMount () { | |||||
if (!this.props.account) { | |||||
this.context.router.replace('/'); | |||||
} | |||||
}, | |||||
componentDidMount () { | |||||
if (!this.props.account) { | |||||
return; | |||||
} | |||||
this.props.dispatch(fetchAccountTimeline(this.props.account.get('id'))); | |||||
}, | |||||
componentWillReceiveProps (nextProps) { | |||||
if (this.props.account !== nextProps.account && nextProps.account) { | |||||
this.props.dispatch(fetchAccountTimeline(nextProps.account.get('id'))); | |||||
} | |||||
}, | |||||
handleCommentChange (e) { | |||||
this.props.dispatch(changeReportComment(e.target.value)); | |||||
}, | |||||
handleSubmit () { | |||||
this.props.dispatch(submitReport()); | |||||
this.context.router.replace('/'); | |||||
}, | |||||
render () { | |||||
const { account, comment, intl, statusIds, isSubmitting } = this.props; | |||||
if (!account) { | |||||
return null; | |||||
} | |||||
return ( | |||||
<Column heading={intl.formatMessage(messages.heading)} icon='flag'> | |||||
<ColumnBackButtonSlim /> | |||||
<div className='report' style={{ display: 'flex', flexDirection: 'column', maxHeight: '100%', boxSizing: 'border-box' }}> | |||||
<div className='report__target' style={{ flex: '0 0 auto', padding: '10px' }}> | |||||
<FormattedMessage id='report.target' defaultMessage='Reporting' /> | |||||
<strong>{account.get('acct')}</strong> | |||||
</div> | |||||
<div style={{ flex: '1 1 auto' }} className='scrollable'> | |||||
<div> | |||||
{statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)} | |||||
</div> | |||||
</div> | |||||
<div style={{ flex: '0 0 160px', padding: '10px' }}> | |||||
<textarea | |||||
className='report__textarea' | |||||
placeholder={intl.formatMessage(messages.placeholder)} | |||||
value={comment} | |||||
onChange={this.handleCommentChange} | |||||
style={textareaStyle} | |||||
disabled={isSubmitting} | |||||
/> | |||||
<div style={{ marginTop: '10px', overflow: 'hidden' }}> | |||||
<div style={{ float: 'right' }}><Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} /></div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</Column> | |||||
); | |||||
} | |||||
}); | |||||
export default connect(makeMapStateToProps)(injectIntl(Report)); |
@ -0,0 +1,57 @@ | |||||
import { | |||||
REPORT_INIT, | |||||
REPORT_SUBMIT_REQUEST, | |||||
REPORT_SUBMIT_SUCCESS, | |||||
REPORT_SUBMIT_FAIL, | |||||
REPORT_CANCEL, | |||||
REPORT_STATUS_TOGGLE | |||||
} from '../actions/reports'; | |||||
import Immutable from 'immutable'; | |||||
const initialState = Immutable.Map({ | |||||
new: Immutable.Map({ | |||||
isSubmitting: false, | |||||
account_id: null, | |||||
status_ids: Immutable.Set(), | |||||
comment: '' | |||||
}) | |||||
}); | |||||
export default function reports(state = initialState, action) { | |||||
switch(action.type) { | |||||
case REPORT_INIT: | |||||
return state.withMutations(map => { | |||||
map.setIn(['new', 'isSubmitting'], false); | |||||
map.setIn(['new', 'account_id'], action.account.get('id')); | |||||
if (state.getIn(['new', 'account_id']) !== action.account.get('id')) { | |||||
map.setIn(['new', 'status_ids'], action.status ? Immutable.Set([action.status.get('id')]) : Immutable.Set()); | |||||
map.setIn(['new', 'comment'], ''); | |||||
} else { | |||||
map.updateIn(['new', 'status_ids'], Immutable.Set(), set => set.add(action.status.get('id'))); | |||||
} | |||||
}); | |||||
case REPORT_STATUS_TOGGLE: | |||||
return state.updateIn(['new', 'status_ids'], Immutable.Set(), set => { | |||||
if (action.checked) { | |||||
return set.add(action.statusId); | |||||
} | |||||
return set.remove(action.statusId); | |||||
}); | |||||
case REPORT_SUBMIT_REQUEST: | |||||
return state.setIn(['new', 'isSubmitting'], true); | |||||
case REPORT_SUBMIT_FAIL: | |||||
return state.setIn(['new', 'isSubmitting'], false); | |||||
case REPORT_CANCEL: | |||||
case REPORT_SUBMIT_SUCCESS: | |||||
return state.withMutations(map => { | |||||
map.setIn(['new', 'account_id'], null); | |||||
map.setIn(['new', 'status_ids'], Immutable.Set()); | |||||
map.setIn(['new', 'comment'], ''); | |||||
map.setIn(['new', 'isSubmitting'], false); | |||||
}); | |||||
default: | |||||
return state; | |||||
} | |||||
}; |
@ -0,0 +1,24 @@ | |||||
# frozen_string_literal: true | |||||
class Api::V1::ReportsController < ApiController | |||||
before_action -> { doorkeeper_authorize! :read }, except: [:create] | |||||
before_action -> { doorkeeper_authorize! :write }, only: [:create] | |||||
before_action :require_user! | |||||
respond_to :json | |||||
def index | |||||
@reports = Report.where(account: current_account) | |||||
end | |||||
def create | |||||
status_ids = params[:status_ids].is_a?(Enumerable) ? params[:status_ids] : [params[:status_ids]] | |||||
@report = Report.create!(account: current_account, | |||||
target_account: Account.find(params[:account_id]), | |||||
status_ids: Status.find(status_ids).pluck(:id), | |||||
comment: params[:comment]) | |||||
render :show | |||||
end | |||||
end |
@ -0,0 +1,9 @@ | |||||
# frozen_string_literal: true | |||||
class Report < ApplicationRecord | |||||
belongs_to :account | |||||
belongs_to :target_account, class_name: 'Account' | |||||
scope :unresolved, -> { where(action_taken: false) } | |||||
scope :resolved, -> { where(action_taken: true) } | |||||
end |
@ -0,0 +1,2 @@ | |||||
collection @reports | |||||
extends 'api/v1/reports/show' |
@ -0,0 +1,2 @@ | |||||
object @report | |||||
attributes :id, :action_taken |
@ -0,0 +1,13 @@ | |||||
class CreateReports < ActiveRecord::Migration[5.0] | |||||
def change | |||||
create_table :reports do |t| | |||||
t.integer :account_id, null: false | |||||
t.integer :target_account_id, null: false | |||||
t.integer :status_ids, array: true, null: false, default: [] | |||||
t.text :comment, null: false, default: '' | |||||
t.boolean :action_taken, null: false, default: false | |||||
t.timestamps | |||||
end | |||||
end | |||||
end |
@ -0,0 +1,4 @@ | |||||
Fabricator(:report) do | |||||
comment "You nasty" | |||||
action_taken false | |||||
end |
@ -0,0 +1,5 @@ | |||||
require 'rails_helper' | |||||
RSpec.describe Report, type: :model do | |||||
end |