Skip to content

Commit

Permalink
Implementing redux module and selectors for handling solution reviews.
Browse files Browse the repository at this point in the history
  • Loading branch information
krulis-martin committed Oct 30, 2022
1 parent 4371fe3 commit 751d84b
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 112 deletions.
240 changes: 128 additions & 112 deletions src/pages/SolutionSourceCodes/SolutionSourceCodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -326,6 +329,7 @@ class SolutionSourceCodes extends Component {
secondSolution,
files,
secondFiles,
reviewComments,
fileContentsSelector,
download,
userSolutionsSelector,
Expand Down Expand Up @@ -453,120 +457,130 @@ class SolutionSourceCodes extends Component {
</Row>
)}

<ResourceRenderer resource={files} bulkyLoading>
{filesRaw => (
<ResourceRenderer resource={secondFiles || []} bulkyLoading>
{(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 => (
<SourceCodeBox
key={file.id}
{...file}
download={download}
fileContentsSelector={fileContentsSelector}
diffMode={diffMode}
adjustDiffMapping={this.openMappingDialog}
/>
))}

{diffMode && secondFiles && (
<Modal
show={this.state.mappingDialogOpenFile !== null}
backdrop="static"
onHide={this.closeDialogs}
size="xl">
<Modal.Header closeButton>
<Modal.Title>
<FormattedMessage
id="app.solutionSourceCodes.mappingModal.title"
defaultMessage="Adjust mapping of compared files"
<ResourceRenderer resource={reviewComments} bulkyLoading>
{reviewCommentsRaw =>
console.log(reviewCommentsRaw) || (
<ResourceRenderer resource={files} bulkyLoading>
{filesRaw => (
<ResourceRenderer resource={secondFiles || []} bulkyLoading>
{(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 => (
<SourceCodeBox
key={file.id}
{...file}
download={download}
fileContentsSelector={fileContentsSelector}
diffMode={diffMode}
adjustDiffMapping={this.openMappingDialog}
/>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<h5 className="mb-3">
<code className="mr-2">
{this.state.mappingDialogOpenFile && this.state.mappingDialogOpenFile.name}
</code>{' '}
<FormattedMessage
id="app.solutionSourceCodes.mappingModal.fileIsAssociatedWith"
defaultMessage="file (on the left) is associated with..."
/>
</h5>

<InsetPanel>
{this.state.mappingDialogOpenFile && (
<FormattedMessage
id="app.solutionSourceCodes.mappingModal.explain"
defaultMessage="Select a file from second solution that will be compared with ''<code>{name}</code>'' file from the first solution. Note that changing a mapping between two files may affect how other files are mapped."
values={{
name: this.state.mappingDialogOpenFile.name,
code: content => <code>{content}</code>,
}}
/>
)}
</InsetPanel>

<Table hover>
<tbody>
{secondFiles.map(file => {
const selected =
this.state.mappingDialogDiffWith &&
file.id === this.state.mappingDialogDiffWith.id;
return (
<tr
key={file.id}
className={selected ? 'table-primary' : ''}
onClick={
selected
? null
: () => this.adjustDiffMapping(this.state.mappingDialogOpenFile.id, file.id)
}>
<td className="shrink-col">
<CircleIcon selected={selected} />
</td>
<td>{file.name}</td>
<td className="shrink-col text-muted text-nowrap small">
{revertedIndex && revertedIndex[file.id] && (
<>
<CodeCompareIcon gapRight />
{revertedIndex[file.id].name}
</>
)}
</td>
</tr>
);
})}
</tbody>
</Table>

{this.state.diffMappings && !isEmptyObject(this.state.diffMappings) && (
<div className="text-center">
<Button variant="danger" onClick={this.resetDiffMappings}>
<RefreshIcon gapRight />
<FormattedMessage
id="app.solutionSourceCodes.mappingModal.resetButton"
defaultMessage="Reset mapping"
/>
</Button>
</div>
))}

{diffMode && secondFiles && (
<Modal
show={this.state.mappingDialogOpenFile !== null}
backdrop="static"
onHide={this.closeDialogs}
size="xl">
<Modal.Header closeButton>
<Modal.Title>
<FormattedMessage
id="app.solutionSourceCodes.mappingModal.title"
defaultMessage="Adjust mapping of compared files"
/>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<h5 className="mb-3">
<code className="mr-2">
{this.state.mappingDialogOpenFile && this.state.mappingDialogOpenFile.name}
</code>{' '}
<FormattedMessage
id="app.solutionSourceCodes.mappingModal.fileIsAssociatedWith"
defaultMessage="file (on the left) is associated with..."
/>
</h5>

<InsetPanel>
{this.state.mappingDialogOpenFile && (
<FormattedMessage
id="app.solutionSourceCodes.mappingModal.explain"
defaultMessage="Select a file from second solution that will be compared with ''<code>{name}</code>'' file from the first solution. Note that changing a mapping between two files may affect how other files are mapped."
values={{
name: this.state.mappingDialogOpenFile.name,
code: content => <code>{content}</code>,
}}
/>
)}
</InsetPanel>

<Table hover>
<tbody>
{secondFiles.map(file => {
const selected =
this.state.mappingDialogDiffWith &&
file.id === this.state.mappingDialogDiffWith.id;
return (
<tr
key={file.id}
className={selected ? 'table-primary' : ''}
onClick={
selected
? null
: () =>
this.adjustDiffMapping(
this.state.mappingDialogOpenFile.id,
file.id
)
}>
<td className="shrink-col">
<CircleIcon selected={selected} />
</td>
<td>{file.name}</td>
<td className="shrink-col text-muted text-nowrap small">
{revertedIndex && revertedIndex[file.id] && (
<>
<CodeCompareIcon gapRight />
{revertedIndex[file.id].name}
</>
)}
</td>
</tr>
);
})}
</tbody>
</Table>

{this.state.diffMappings && !isEmptyObject(this.state.diffMappings) && (
<div className="text-center">
<Button variant="danger" onClick={this.resetDiffMappings}>
<RefreshIcon gapRight />
<FormattedMessage
id="app.solutionSourceCodes.mappingModal.resetButton"
defaultMessage="Reset mapping"
/>
</Button>
</div>
)}
</Modal.Body>
</Modal>
)}
</Modal.Body>
</Modal>
)}
</>
);
}}
</ResourceRenderer>
)}
</>
);
}}
</ResourceRenderer>
)}
</ResourceRenderer>
)
}
</ResourceRenderer>

<Modal show={this.state.diffDialogOpen} backdrop="static" onHide={this.closeDialogs} size="xl">
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
41 changes: 41 additions & 0 deletions src/redux/modules/solutionReviews.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions src/redux/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -103,6 +104,7 @@ const createRecodexReducers = (token, instanceId, lang) => ({
sisTerms,
solutions,
solutionFiles,
solutionReviews,
stats,
submission,
submissionEvaluations,
Expand Down
9 changes: 9 additions & 0 deletions src/redux/selectors/solutionReviews.js
Original file line number Diff line number Diff line change
@@ -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)
);

0 comments on commit 751d84b

Please sign in to comment.