Skip to content

Commit

Permalink
Use AlertsList React component in /alerts_list
Browse files Browse the repository at this point in the history
fix for #1803

* Refactors AlertsList component into seperate CampaignAlerts and AdminAlerts components with shared functionality
* Allows for asynchronous 'resolving' for AdminAlerts
* Campaign alerts functionality not changed
* Sort options determined from available alerts to sort
  • Loading branch information
chrisnorwood committed Nov 13, 2019
1 parent b7ab7c8 commit 6874941
Show file tree
Hide file tree
Showing 14 changed files with 255 additions and 80 deletions.
52 changes: 49 additions & 3 deletions app/assets/javascripts/actions/alert_actions.js
Expand Up @@ -27,7 +27,53 @@ const fetchResponseToJSON = (res) => {
return Promise.reject(res);
};

const fetchAlertsPromise = (campaignSlug) => {
const resolveAlertPromise = (alertId) => {
return fetch(`/alerts/${alertId}/resolve.json`, {
credentials: 'include'
}).then(fetchResponseToJSON)
.catch((error) => {
logErrorMessage(error);
return error;
});
};

export const handleResolveAlert = alertId => (dispatch) => {
return (
resolveAlertPromise(alertId)
.then(() => {
dispatch({
type: types.RESOLVE_ALERT,
alertId
});
})
.catch(response => (dispatch({ type: types.API_FAIL, data: response })))
);
};

const fetchAdminAlertsPromise = () => {
return fetch('/alerts_list.json', {
credentials: 'include'
}).then(fetchResponseToJSON)
.catch((error) => {
logErrorMessage(error);
return error;
});
};

export const fetchAdminAlerts = () => (dispatch) => {
return (
fetchAdminAlertsPromise()
.then((data) => {
dispatch({
type: types.RECEIVE_ALERTS,
data
});
})
.catch(response => (dispatch({ type: types.API_FAIL, data: response })))
);
};

const fetchCampaignAlertsPromise = (campaignSlug) => {
return fetch(`/campaigns/${campaignSlug}/alerts.json`, {
credentials: 'include'
}).then(fetchResponseToJSON)
Expand All @@ -37,9 +83,9 @@ const fetchAlertsPromise = (campaignSlug) => {
});
};

export const fetchAlerts = campaignSlug => (dispatch) => {
export const fetchCampaignAlerts = campaignSlug => (dispatch) => {
return (
fetchAlertsPromise(campaignSlug)
fetchCampaignAlertsPromise(campaignSlug)
.then((data) => {
dispatch({
type: types.RECEIVE_ALERTS,
Expand Down
31 changes: 31 additions & 0 deletions app/assets/javascripts/components/alerts/admin_alerts.jsx
@@ -0,0 +1,31 @@
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

import AlertsHandler from './alerts_handler.jsx';
import { fetchAdminAlerts } from '../../actions/alert_actions';

const AdminAlerts = createReactClass({
displayName: 'AdminAlerts',
propTypes: {
fetchAlerts: PropTypes.func,
},
UNSAFE_componentWillMount() {
// This adds ALL alerts to the state, to be used in AlertsHandler
this.props.fetchAdminAlerts();
},
render() {
return (
<AlertsHandler
alertLabel={I18n.t('alerts.alert_label')}
noAlertsLabel={I18n.t('alerts.no_alerts')}
adminAlert={true}
/>
);
}
});

const mapDispatchToProps = { fetchAdminAlerts };

export default connect(null, mapDispatchToProps)(AdminAlerts);
42 changes: 38 additions & 4 deletions app/assets/javascripts/components/alerts/alert.jsx
@@ -1,21 +1,55 @@
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { connect } from 'react-redux';

import { handleResolveAlert } from '../../actions/alert_actions';

const Alert = ({ alert, adminAlert, resolveAlert }) => {
let resolveCell;
let alertTypeCell;

if (adminAlert) {
let resolveText;
let resolveButton;
if (alert.resolved) {
resolveText = '✓';
}
if (alert.resolvable && !alert.resolved) {
resolveButton = (
<button className="button small danger dark" onClick={() => resolveAlert(alert.id)}>Resolve</button>
);
}
resolveCell = (
<td className="desktop-only-tc">{resolveText} {resolveButton}</td>
);
alertTypeCell = (
<td className="alert-type">
<a href={`/alerts_list/${alert.id}`}>{alert.type}</a>
</td>
);
} else {
alertTypeCell = <td className="alert-type">{alert.type}</td>;
}

const Alert = ({ alert }) => {
return (
<tr className="alert">
<td className="desktop-only-tc date">{moment(alert.created_at).format('YYYY-MM-DD h:mm A')}</td>
<td className="alert-type">{alert.type}</td>
{alertTypeCell}
<td className="desktop-only-tc"><a target="_blank" href={`/courses/${alert.course_slug}`}>{alert.course}</a></td>
<td className="desktop-only-tc"><a target="_blank" href={`/users/${alert.user}`}>{alert.user}</a></td>
<td><a target="_blank" href={alert.article_url}>{alert.article}</a></td>
{resolveCell}
</tr>
);
};

Alert.propTypes = {
alert: PropTypes.object
alert: PropTypes.object,
adminAlert: PropTypes.bool,
resolveAlert: PropTypes.func,
};

export default Alert;
const mapDispatchToProps = { resolveAlert: handleResolveAlert };

export default connect(null, mapDispatchToProps)(Alert);
64 changes: 30 additions & 34 deletions app/assets/javascripts/components/alerts/alerts_handler.jsx
Expand Up @@ -4,45 +4,35 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';

import AlertsList from './alerts_list.jsx';
import { fetchAlerts, sortAlerts, filterAlerts } from '../../actions/alert_actions';
import { sortAlerts, filterAlerts } from '../../actions/alert_actions';
import MultiSelectField from '../common/multi_select_field.jsx';
import { getFilteredAlerts } from '../../selectors';

const ALERTS = [
{ label: 'Active Course', value: 'ActiveCourseAlert' },
{ label: 'Articles For Deletion', value: 'ArticlesForDeletionAlert' },
{ label: 'Blocked Edits', value: 'BlockedEditsAlert' },
{ label: 'Blocked User', value: 'BlockedUserAlert' },
{ label: 'Continued Course Activity', value: 'ContinuedCourseActivityAlert' },
{ label: 'Deleted Uploads', value: 'DeletedUploadsAlert' },
{ label: 'Discretionary Sanctions Edit', value: 'DiscretionarySanctionsEditAlert' },
{ label: 'DYK Nomination', value: 'DYKNominationAlert' },
{ label: 'GA Nomination', value: 'GANominationAlert' },
{ label: 'No Enrolled Students', value: 'NoEnrolledStudentsAlert' },
{ label: 'Productive Course', value: 'ProductiveCourseAlert' },
{ label: 'Unsubmitted Course', value: 'UnsubmittedCourseAlert' },
{ label: 'Untrained Students', value: 'UntrainedStudentsAlert' },
];
// This helper function takes in an array of alert objects as input,
// and outputs an array objects in the format { label: '', value: '' }
// to be used by the MultiSelectField component
const transformAlertsIntoOptions = (alertsArray) => {
if (alertsArray.length === 0) return [];

return alertsArray.map((item) => {
const value = item.type;
const labelWordArray = value.split(/(?=[A-Z][a-z])/);
labelWordArray.pop();
const label = labelWordArray.join(' ');

return { label, value };
});
};

const AlertsHandler = createReactClass({
displayName: 'AlertsHandler',

propTypes: {
fetchAlerts: PropTypes.func,
alerts: PropTypes.array,
},

getCampaignSlug() {
return `${this.props.match.params.campaign_slug}`;
},

UNSAFE_componentWillMount() {
const campaignSlug = this.getCampaignSlug();
return this.props.fetchAlerts(campaignSlug);
},

fetchAlerts(campaignSlug) {
this.props.fetchAlerts(campaignSlug);
alertLabel: PropTypes.string,
noAlertsLabel: PropTypes.string,
adminAlert: PropTypes.bool,
alertTypes: PropTypes.array,
},

sortSelect(e) {
Expand All @@ -59,8 +49,8 @@ const AlertsHandler = createReactClass({
alertList = (
<div id="alerts" className="campaign_main alerts container">
<div className="section-header">
<h3>{I18n.t('campaign.alert_label')}</h3>
<MultiSelectField options={ALERTS} label={I18n.t('campaign.alert_select_label')} selected={this.props.selectedFilters} setSelectedFilters={this.filterAlerts} />
<h3>{this.props.alertLabel}</h3>
<MultiSelectField options={this.props.alertTypes} label={I18n.t('campaign.alert_select_label')} selected={this.props.selectedFilters} setSelectedFilters={this.filterAlerts} />
<div className="sort-select">
<select className="sorts" name="sorts" onChange={this.sortSelect}>
<option value="type">{I18n.t('campaign.alert_type')}</option>
Expand All @@ -70,7 +60,12 @@ const AlertsHandler = createReactClass({
</select>
</div>
</div>
<AlertsList alerts={this.props.selectedAlerts} sortBy={this.props.sortAlerts} />
<AlertsList
alerts={this.props.selectedAlerts}
sortBy={this.props.sortAlerts}
noAlertsLabel={this.props.noAlertsLabel}
adminAlert={this.props.adminAlert ? this.props.adminAlert : false}
/>
</div>
);
}
Expand All @@ -84,8 +79,9 @@ const mapStateToProps = state => ({
alerts: state.alerts.alerts,
selectedFilters: state.alerts.selectedFilters,
selectedAlerts: getFilteredAlerts(state),
alertTypes: transformAlertsIntoOptions(state.alerts.alerts),
});

const mapDispatchToProps = { fetchAlerts, sortAlerts, filterAlerts };
const mapDispatchToProps = { sortAlerts, filterAlerts };

export default connect(mapStateToProps, mapDispatchToProps)(AlertsHandler);
19 changes: 14 additions & 5 deletions app/assets/javascripts/components/alerts/alerts_list.jsx
Expand Up @@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
import List from '../common/list.jsx';
import Alert from './alert.jsx';

const AlertsList = ({ alerts, sortBy }) => {
const AlertsList = ({ alerts, sortBy, noAlertsLabel, adminAlert }) => {
const elements = alerts.map((alert) => {
return <Alert alert={alert} key={alert.id} />;
return <Alert alert={alert} key={alert.id} adminAlert={adminAlert} />;
});

const keys = {
Expand All @@ -27,22 +27,31 @@ const AlertsList = ({ alerts, sortBy }) => {
article: {
label: I18n.t('campaign.alert_article'),
desktop_only: false
}
},
};

if (adminAlert) {
keys.resolve = {
label: 'Resolve',
desktop_only: false
};
}

return (
<List
elements={elements}
keys={keys}
table_key="alerts"
none_message={I18n.t('campaign.no_alerts')}
none_message={noAlertsLabel}
sortBy={sortBy}
/>
);
};

AlertsList.propTypes = {
alerts: PropTypes.array
alerts: PropTypes.array,
noAlertsLabel: PropTypes.string,
adminAlert: PropTypes.bool,
};

export default AlertsList;
39 changes: 39 additions & 0 deletions app/assets/javascripts/components/alerts/campaign_alerts.jsx
@@ -0,0 +1,39 @@
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

import AlertsHandler from './alerts_handler.jsx';
import { fetchCampaignAlerts, filterAlerts } from '../../actions/alert_actions';

const CampaignAlerts = createReactClass({
displayName: 'CampaignAlerts',
propTypes: {
fetchAlerts: PropTypes.func,
filterAlerts: PropTypes.func,
},
getCampaignSlug() {
return `${this.props.match.params.campaign_slug}`;
},
UNSAFE_componentWillMount() {
// This adds the specific campaign alerts to the state, to be used in AlertsHandler
this.props.fetchCampaignAlerts(this.getCampaignSlug());
this.props.filterAlerts(this.defaultFilters);
},
defaultFilters: [
{ value: 'ArticlesForDeletionAlert', label: 'Articles For Deletion' },
{ value: 'DiscretionarySanctionsEditAlert', label: 'Discretionary Sanctions' }
],
render() {
return (
<AlertsHandler
alertLabel={I18n.t('campaign.alert_label')}
noAlertsLabel={I18n.t('campaign.no_alerts')}
/>
);
}
});

const mapDispatchToProps = { fetchCampaignAlerts, filterAlerts };

export default connect(null, mapDispatchToProps)(CampaignAlerts);
6 changes: 4 additions & 2 deletions app/assets/javascripts/components/util/routes.jsx
Expand Up @@ -5,7 +5,8 @@ import Course from '../course/course.jsx';
import Onboarding from '../onboarding/index.jsx';
import { ConnectedCourseCreator } from '../course_creator/course_creator.jsx';
import ArticleFinder from '../article_finder/article_finder.jsx';
import AlertsHandler from '../alerts/alerts_handler.jsx';
import CampaignAlerts from '../alerts/campaign_alerts.jsx';
import AdminAlerts from '../alerts/admin_alerts.jsx';
import CampaignOverviewHandler from '../campaign/campaign_overview_handler.jsx';
import CampaignOresPlot from '../campaign/campaign_ores_plot.jsx';
import RecentActivityHandler from '../activity/recent_activity_handler.jsx';
Expand All @@ -23,8 +24,9 @@ const routes = (
<Route path="/course_creator" component={ConnectedCourseCreator} />
<Route path="/users/:username" component={UserProfile} />
<Route path="/campaigns/:campaign_slug/overview" component={CampaignOverviewHandler} />
<Route path="/campaigns/:campaign_slug/alerts" component={AlertsHandler} />
<Route path="/campaigns/:campaign_slug/alerts" component={CampaignAlerts} />
<Route path="/campaigns/:campaign_slug/ores_plot" component={CampaignOresPlot} />
<Route path="/alerts_list" component={AdminAlerts} />
<Route path="/settings" component={SettingsHandler} />
<Route path="/article_finder" component={ArticleFinder} />
<Route path="/training" component={TrainingApp} />
Expand Down
1 change: 1 addition & 0 deletions app/assets/javascripts/constants/alert.js
Expand Up @@ -2,3 +2,4 @@ export const RECEIVE_ALERTS = 'RECEIVE_ALERTS';
export const SORT_ALERTS = 'SORT_ALERTS';
export const FILTER_ALERTS = 'FILTER_ALERTS';
export const FETCH_ONBOARDING_ALERT = 'FETCH_ONBOARDING_ALERT';
export const RESOLVE_ALERT = 'RESOLVE_ALERT';

0 comments on commit 6874941

Please sign in to comment.