diff --git a/src/components/SubmissionFailures/FailuresList/FailuresList.js b/src/components/SubmissionFailures/FailuresList/FailuresList.js new file mode 100644 index 000000000..0075e9b16 --- /dev/null +++ b/src/components/SubmissionFailures/FailuresList/FailuresList.js @@ -0,0 +1,72 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Table } from 'react-bootstrap'; +import { FormattedMessage } from 'react-intl'; +import FailuresListItem from '../FailuresListItem'; + +const FailuresList = ({ failures, createActions }) => + + + + + + + + + + + + {failures.map((failure, i) => + + )} + + {failures.length === 0 && + + + } + +
+ + + + + + + + + + +
+ +
; + +FailuresList.propTypes = { + failures: PropTypes.array, + createActions: PropTypes.func, + failure: PropTypes.object +}; + +export default FailuresList; diff --git a/src/components/SubmissionFailures/FailuresList/index.js b/src/components/SubmissionFailures/FailuresList/index.js new file mode 100644 index 000000000..5290225a3 --- /dev/null +++ b/src/components/SubmissionFailures/FailuresList/index.js @@ -0,0 +1 @@ +export default from './FailuresList'; diff --git a/src/components/SubmissionFailures/FailuresListItem/FailuresListItem.js b/src/components/SubmissionFailures/FailuresListItem/FailuresListItem.js new file mode 100644 index 000000000..c4b01c858 --- /dev/null +++ b/src/components/SubmissionFailures/FailuresListItem/FailuresListItem.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedDate, FormattedTime } from 'react-intl'; +import Icon from 'react-fontawesome'; +import { OverlayTrigger, Tooltip } from 'react-bootstrap'; + +const FailuresListItem = ({ id, createActions, failure }) => + + + + {failure.type} + + } + > +
+ {failure.type === 'broker_reject' && } + {failure.type === 'evaluation_failure' && } + {failure.type === 'loading_failure' && } +
+
+ + + {failure.description} + + + + {', '} + + + + {failure.resolvedAt + ? + + {', '} + + + : } + + + {failure.resolutionNote + ? + {failure.resolutionNote} + + : } + + + {createActions && createActions(id)} + + ; + +FailuresListItem.propTypes = { + id: PropTypes.string.isRequired, + failure: PropTypes.object.isRequired, + createActions: PropTypes.func +}; + +export default FailuresListItem; diff --git a/src/components/SubmissionFailures/FailuresListItem/index.js b/src/components/SubmissionFailures/FailuresListItem/index.js new file mode 100644 index 000000000..87ad69694 --- /dev/null +++ b/src/components/SubmissionFailures/FailuresListItem/index.js @@ -0,0 +1 @@ +export default from './FailuresListItem'; diff --git a/src/components/SubmissionFailures/ResolveFailure/ResolveFailure.js b/src/components/SubmissionFailures/ResolveFailure/ResolveFailure.js new file mode 100644 index 000000000..ac6a4eeef --- /dev/null +++ b/src/components/SubmissionFailures/ResolveFailure/ResolveFailure.js @@ -0,0 +1,105 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Modal, Button } from 'react-bootstrap'; +import { CloseIcon } from '../../icons'; +import { FormattedMessage } from 'react-intl'; +import { Field, reduxForm } from 'redux-form'; +import TextField from '../../forms/Fields/TextField'; +import SubmitButton from '../../forms/SubmitButton'; + +const maxNoteLength = value => + value && value.length >= 255 + ? + : undefined; + +const ResolveFailure = ({ + isOpen, + onClose, + submitting, + handleSubmit, + anyTouched, + submitFailed = false, + submitSucceeded = false, + invalid, + reset +}) => + + + + + + + + + } + validate={maxNoteLength} + /> + + + handleSubmit(data).then(() => reset())} + submitting={submitting} + dirty={anyTouched} + hasSucceeded={submitSucceeded} + hasFailed={submitFailed} + invalid={invalid} + messages={{ + submit: ( + + ), + submitting: ( + + ), + success: ( + + ) + }} + /> + + + + ; + +ResolveFailure.propTypes = { + handleSubmit: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + submitFailed: PropTypes.bool, + anyTouched: PropTypes.bool, + submitSucceeded: PropTypes.bool, + submitting: PropTypes.bool, + invalid: PropTypes.bool, + reset: PropTypes.func, + onClose: PropTypes.func.isRequired, + isOpen: PropTypes.bool.isRequired +}; + +export default reduxForm({ form: 'resolve-failure' })(ResolveFailure); diff --git a/src/components/SubmissionFailures/ResolveFailure/index.js b/src/components/SubmissionFailures/ResolveFailure/index.js new file mode 100644 index 000000000..6d3c96060 --- /dev/null +++ b/src/components/SubmissionFailures/ResolveFailure/index.js @@ -0,0 +1 @@ +export default from './ResolveFailure'; diff --git a/src/components/layout/Sidebar/Admin.js b/src/components/layout/Sidebar/Admin.js index 65d835bd3..d59a05c70 100644 --- a/src/components/layout/Sidebar/Admin.js +++ b/src/components/layout/Sidebar/Admin.js @@ -7,7 +7,10 @@ import MenuItem from '../../widgets/Sidebar/MenuItem'; import withLinks from '../../../hoc/withLinks'; -const Admin = ({ currentUrl, links: { ADMIN_INSTANCES_URI, USERS_URI } }) => ( +const Admin = ({ + currentUrl, + links: { ADMIN_INSTANCES_URI, USERS_URI, FAILURES_URI } +}) =>
    ( currentPath={currentUrl} link={USERS_URI} /> -
-); + + } + currentPath={currentUrl} + link={FAILURES_URI} + /> + ; Admin.propTypes = { currentUrl: PropTypes.string.isRequired, diff --git a/src/links/index.js b/src/links/index.js index 3ed26496b..ea5b3648a 100644 --- a/src/links/index.js +++ b/src/links/index.js @@ -82,6 +82,9 @@ export const linksFactory = lang => { const ADMIN_EDIT_INSTANCE_URI_FACTORY = instanceId => `${ADMIN_INSTANCES_URI}/${instanceId}/edit`; + // failures details + const FAILURES_URI = `${prefix}/app/submission-failures`; + // download files const DOWNLOAD = fileId => `${API_BASE}/uploaded-files/${fileId}/download`; @@ -127,7 +130,8 @@ export const linksFactory = lang => { ADMIN_EDIT_INSTANCE_URI_FACTORY, DOWNLOAD, REFERENCE_SOLUTION_EVALUATION_URI_FACTORY, - LOGIN_EXTERN_FINALIZATION + LOGIN_EXTERN_FINALIZATION, + FAILURES_URI }; }; diff --git a/src/pages/SubmissionFailures/SubmissionFailures.js b/src/pages/SubmissionFailures/SubmissionFailures.js new file mode 100644 index 000000000..b7fba8813 --- /dev/null +++ b/src/pages/SubmissionFailures/SubmissionFailures.js @@ -0,0 +1,156 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; + +import PageContent from '../../components/layout/PageContent'; +import FetchManyResourceRenderer from '../../components/helpers/FetchManyResourceRenderer'; +import { + fetchAllFailures, + resolveFailure +} from '../../redux/modules/submissionFailures'; +import { + fetchManyStatus, + readySubmissionFailuresSelector +} from '../../redux/selectors/submissionFailures'; +import FailuresList from '../../components/SubmissionFailures/FailuresList/FailuresList'; +import Box from '../../components/widgets/Box/Box'; +import ResolveFailure from '../../components/SubmissionFailures/ResolveFailure/ResolveFailure'; +import { Button } from 'react-bootstrap'; + +class SubmissionFailures extends Component { + state = { isOpen: false, activeId: null }; + + static loadAsync = (params, dispatch) => + Promise.all([dispatch(fetchAllFailures)]); + + componentWillMount() { + this.props.loadAsync(); + } + + render() { + const { submissionFailures, fetchStatus, resolveFailure } = this.props; + + return ( + + } + description={ + + } + /> + } + failed={ + + } + description={ + + } + /> + } + > + {() => + + } + description={ + + } + breadcrumbs={[ + { + text: ( + + ), + iconName: 'fort-awesome' + } + ]} + > + + } + unlimitedHeight + noPadding + > +
+ + } + /> + + this.setState({ isOpen: false, activeId: null })} + onSubmit={data => + resolveFailure(this.state.activeId, data.note).then(() => + this.setState({ isOpen: false, activeId: null }) + )} + /> +
+
+
} +
+ ); + } +} + +SubmissionFailures.propTypes = { + loadAsync: PropTypes.func.isRequired, + fetchStatus: PropTypes.string, + submissionFailures: PropTypes.array, + resolveFailure: PropTypes.func +}; + +export default connect( + state => ({ + fetchStatus: fetchManyStatus(state), + submissionFailures: readySubmissionFailuresSelector(state) + }), + dispatch => ({ + loadAsync: () => SubmissionFailures.loadAsync({}, dispatch), + resolveFailure: (id, note) => dispatch(resolveFailure(id, note)) + }) +)(SubmissionFailures); diff --git a/src/pages/SubmissionFailures/index.js b/src/pages/SubmissionFailures/index.js new file mode 100644 index 000000000..8f4a46d73 --- /dev/null +++ b/src/pages/SubmissionFailures/index.js @@ -0,0 +1 @@ +export default from './SubmissionFailures'; diff --git a/src/pages/routes.js b/src/pages/routes.js index 5336beb4d..0de0f3f4d 100644 --- a/src/pages/routes.js +++ b/src/pages/routes.js @@ -38,6 +38,7 @@ import Pipelines from './Pipelines'; import EditPipeline from './EditPipeline'; import Pipeline from './Pipeline'; import FAQ from './FAQ'; +import SubmissionFailures from './SubmissionFailures/SubmissionFailures'; import ChangePassword from './ChangePassword'; import ResetPassword from './ResetPassword'; @@ -137,6 +138,9 @@ const createRoutes = getState => { + + + diff --git a/src/redux/modules/submissionFailures.js b/src/redux/modules/submissionFailures.js new file mode 100644 index 000000000..db262bae8 --- /dev/null +++ b/src/redux/modules/submissionFailures.js @@ -0,0 +1,46 @@ +import { handleActions } from 'redux-actions'; +import factory, { initialState } from '../helpers/resourceManager'; +import { createApiAction } from '../middleware/apiMiddleware'; +import { fromJS } from 'immutable'; + +const resourceName = 'submissionFailures'; +var { actions, reduceActions } = factory({ resourceName }); + +export const additionalActionTypes = { + RESOLVE: 'recodex/submissionFailures/RESOLVE', + RESOLVE_FULFILLED: 'recodex/submissionFailures/RESOLVE_FULFILLED' +}; + +/** + * Actions + */ +export const fetchManyEndpoint = '/submission-failures'; + +export const fetchAllFailures = actions.fetchMany({ + endpoint: fetchManyEndpoint +}); + +export const resolveFailure = (id, note) => + createApiAction({ + type: additionalActionTypes.RESOLVE, + method: 'POST', + endpoint: `/submission-failures/${id}/resolve`, + body: { note }, + meta: { id } + }); + +/** + * Reducer takes mainly care about all the state of individual attachments + */ + +const reducer = handleActions( + Object.assign({}, reduceActions, { + [additionalActionTypes.RESOLVE_FULFILLED]: ( + state, + { meta: { id }, payload } + ) => state.setIn(['resources', id, 'data'], fromJS(payload)) + }), + initialState +); + +export default reducer; diff --git a/src/redux/reducer.js b/src/redux/reducer.js index 18cda1b42..0300459ec 100644 --- a/src/redux/reducer.js +++ b/src/redux/reducer.js @@ -46,6 +46,7 @@ import sisSupervisedCourses from './modules/sisSupervisedCourses'; import sisPossibleParents from './modules/sisPossibleParents'; import referenceSolutionEvaluations from './modules/referenceSolutionEvaluations'; import submissionEvaluations from './modules/submissionEvaluations'; +import submissionFailures from './modules/submissionFailures'; const createRecodexReducers = token => ({ auth: auth(token), @@ -91,7 +92,8 @@ const createRecodexReducers = token => ({ sisSubscribedGroups, sisSupervisedCourses, sisPossibleParents, - submissionEvaluations + submissionEvaluations, + submissionFailures }); const librariesReducers = { diff --git a/src/redux/selectors/submissionFailures.js b/src/redux/selectors/submissionFailures.js new file mode 100644 index 000000000..64d02bf16 --- /dev/null +++ b/src/redux/selectors/submissionFailures.js @@ -0,0 +1,31 @@ +import { createSelector } from 'reselect'; +import { fetchManyEndpoint } from '../modules/submissionFailures'; +import { isReady, getJsData } from '../helpers/resourceManager'; + +const getSubmissionFailures = state => state.submissionFailures; +const getResources = failures => failures.get('resources'); + +export const submissionFailuresSelector = createSelector( + getSubmissionFailures, + getResources +); + +export const fetchManyStatus = createSelector(getSubmissionFailures, state => + state.getIn(['fetchManyStatus', fetchManyEndpoint]) +); + +export const getSubmissionFailure = failureId => + createSelector(submissionFailuresSelector, failures => + failures.get(failureId) + ); + +export const readySubmissionFailuresSelector = createSelector( + submissionFailuresSelector, + failures => + failures + .toList() + .filter(isReady) + .map(getJsData) + .sort((a, b) => b.createdAt - a.createdAt) + .toArray() +);