@ -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 |