parent
40a4053732
commit
3b81baaaaf
@ -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 |
Loading…
Reference in new issue