diff --git a/src/pages/SolutionSourceCodes/SolutionSourceCodes.js b/src/pages/SolutionSourceCodes/SolutionSourceCodes.js index 71ed3a7b5..fe92d71eb 100644 --- a/src/pages/SolutionSourceCodes/SolutionSourceCodes.js +++ b/src/pages/SolutionSourceCodes/SolutionSourceCodes.js @@ -31,10 +31,12 @@ import { fetchRuntimeEnvironments } from '../../redux/modules/runtimeEnvironment import { fetchAssignmentIfNeeded } from '../../redux/modules/assignments'; import { fetchSolutionIfNeeded, fetchUsersSolutions } from '../../redux/modules/solutions'; import { fetchAssignmentSolutionFilesIfNeeded } from '../../redux/modules/solutionFiles'; +import { fetchSolutionReviewIfNeeded } from '../../redux/modules/solutionReviews'; import { download } from '../../redux/modules/files'; import { fetchContentIfNeeded } from '../../redux/modules/filesContent'; import { getSolution } from '../../redux/selectors/solutions'; import { getSolutionFiles } from '../../redux/selectors/solutionFiles'; +import { getSolutionReviewComments } from '../../redux/selectors/solutionReviews'; import { getAssignment, getUserSolutionsSortedData, @@ -201,6 +203,7 @@ class SolutionSourceCodes extends Component { registerSolutionVisit(solution); return Promise.all([dispatch(fetchUsersSolutions(solution.authorId, assignmentId))]); }), + dispatch(fetchSolutionReviewIfNeeded(solutionId)), dispatch(fetchAssignmentIfNeeded(assignmentId)), dispatch(fetchAssignmentSolutionFilesIfNeeded(solutionId)) .then(res => preprocessFiles(res.value)) @@ -326,6 +329,7 @@ class SolutionSourceCodes extends Component { secondSolution, files, secondFiles, + reviewComments, fileContentsSelector, download, userSolutionsSelector, @@ -453,120 +457,130 @@ class SolutionSourceCodes extends Component { )} - - {filesRaw => ( - - {(secondFilesRaw = null) => { - const secondFiles = secondFilesRaw && preprocessFiles(secondFilesRaw); - const files = associateFilesForDiff( - preprocessFiles(filesRaw), - secondFiles, - this.state.diffMappings - ); - const revertedIndex = files && secondFiles && getRevertedMapping(files); - return ( - <> - {files.map(file => ( - - ))} - - {diffMode && secondFiles && ( - - - - + {reviewCommentsRaw => + console.log(reviewCommentsRaw) || ( + + {filesRaw => ( + + {(secondFilesRaw = null) => { + const secondFiles = secondFilesRaw && preprocessFiles(secondFilesRaw); + const files = associateFilesForDiff( + preprocessFiles(filesRaw), + secondFiles, + this.state.diffMappings + ); + const revertedIndex = files && secondFiles && getRevertedMapping(files); + return ( + <> + {files.map(file => ( + - - - -
- - {this.state.mappingDialogOpenFile && this.state.mappingDialogOpenFile.name} - {' '} - -
- - - {this.state.mappingDialogOpenFile && ( - {content}, - }} - /> - )} - - - - - {secondFiles.map(file => { - const selected = - this.state.mappingDialogDiffWith && - file.id === this.state.mappingDialogDiffWith.id; - return ( - this.adjustDiffMapping(this.state.mappingDialogOpenFile.id, file.id) - }> - - - - - ); - })} - -
- - {file.name} - {revertedIndex && revertedIndex[file.id] && ( - <> - - {revertedIndex[file.id].name} - - )} -
- - {this.state.diffMappings && !isEmptyObject(this.state.diffMappings) && ( -
- -
+ ))} + + {diffMode && secondFiles && ( + + + + + + + +
+ + {this.state.mappingDialogOpenFile && this.state.mappingDialogOpenFile.name} + {' '} + +
+ + + {this.state.mappingDialogOpenFile && ( + {content}, + }} + /> + )} + + + + + {secondFiles.map(file => { + const selected = + this.state.mappingDialogDiffWith && + file.id === this.state.mappingDialogDiffWith.id; + return ( + + this.adjustDiffMapping( + this.state.mappingDialogOpenFile.id, + file.id + ) + }> + + + + + ); + })} + +
+ + {file.name} + {revertedIndex && revertedIndex[file.id] && ( + <> + + {revertedIndex[file.id].name} + + )} +
+ + {this.state.diffMappings && !isEmptyObject(this.state.diffMappings) && ( +
+ +
+ )} +
+
)} -
-
- )} - - ); - }} -
- )} + + ); + }} +
+ )} + + ) + } @@ -650,6 +664,7 @@ SolutionSourceCodes.propTypes = { solution: ImmutablePropTypes.map, secondSolution: ImmutablePropTypes.map, files: ImmutablePropTypes.map, + reviewComments: ImmutablePropTypes.map, secondFiles: ImmutablePropTypes.map, fileContentsSelector: PropTypes.func.isRequired, userSolutionsSelector: PropTypes.func.isRequired, @@ -680,6 +695,7 @@ export default withLinks( solution: getSolution(state, solutionId), secondSolution, files: getSolutionFiles(state, solutionId), + reviewComments: getSolutionReviewComments(state, solutionId), secondFiles: secondSolutionId && secondSolutionId !== solutionId ? getSolutionFiles(state, secondSolutionId) : null, fileContentsSelector: getFilesContentSelector(state), diff --git a/src/redux/modules/solutionReviews.js b/src/redux/modules/solutionReviews.js new file mode 100644 index 000000000..2d6e0d27f --- /dev/null +++ b/src/redux/modules/solutionReviews.js @@ -0,0 +1,41 @@ +import { handleActions, createAction } from 'redux-actions'; +import { fromJS } from 'immutable'; + +import { createApiAction } from '../middleware/apiMiddleware'; +import factory, { initialState, createRecord, resourceStatus } from '../helpers/resourceManager'; +import { actionTypes as submissionActionTypes } from './submission'; +import { actionTypes as submissionEvaluationActionTypes } from './submissionEvaluations'; +import { getAssignmentSolversLastUpdate } from '../selectors/solutions'; +import { objectFilter } from '../../helpers/common'; + +const resourceName = 'solutionReviews'; + +const apiEndpointFactory = id => `/assignment-solutions/${id}/review`; +const { actions, actionTypes, reduceActions } = factory({ + resourceName, + apiEndpointFactory, +}); + +/** + * Actions + */ +export { actionTypes }; +export const additionalActionTypes = {}; + +export const fetchSolutionReview = actions.fetchResource; +export const fetchSolutionReviewIfNeeded = actions.fetchOneIfNeeded; +export const deleteSolutionReview = actions.removeResource; + +/** + * Reducer + */ + +const reducer = handleActions( + Object.assign({}, reduceActions, { + [actionTypes.FETCH_FULFILLED]: (state, { meta: { id }, payload: { reviewComments: data } }) => + state.setIn(['resources', id], createRecord({ state: resourceStatus.FULFILLED, data })), + }), + initialState +); + +export default reducer; diff --git a/src/redux/reducer.js b/src/redux/reducer.js index 7f732d5e8..f7557987b 100644 --- a/src/redux/reducer.js +++ b/src/redux/reducer.js @@ -45,6 +45,7 @@ import sisSupervisedCourses from './modules/sisSupervisedCourses'; import sisTerms from './modules/sisTerms'; import solutions from './modules/solutions'; import solutionFiles from './modules/solutionFiles'; +import solutionReviews from './modules/solutionReviews'; import stats from './modules/stats'; import submission from './modules/submission'; import submissionEvaluations from './modules/submissionEvaluations'; @@ -103,6 +104,7 @@ const createRecodexReducers = (token, instanceId, lang) => ({ sisTerms, solutions, solutionFiles, + solutionReviews, stats, submission, submissionEvaluations, diff --git a/src/redux/selectors/solutionReviews.js b/src/redux/selectors/solutionReviews.js new file mode 100644 index 000000000..28514fd32 --- /dev/null +++ b/src/redux/selectors/solutionReviews.js @@ -0,0 +1,9 @@ +import { createSelector } from 'reselect'; + +const getSolutionsReviewsResources = state => state.solutionReviews.get('resources'); +const getParam = (_, id) => id; + +export const getSolutionReviewComments = createSelector( + [getSolutionsReviewsResources, getParam], + (reviewComments, id) => reviewComments.get(id) +);