parent
94a6dfab3a
commit
2cc31b3194
@ -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