Skip to content

Commit

Permalink
Merge pull request #233 from kbss-cvut/205-reject-message
Browse files Browse the repository at this point in the history
Reject Message
  • Loading branch information
blcham authored Nov 11, 2024
2 parents 272f43d + 726b60a commit 15be05f
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 19 deletions.
29 changes: 29 additions & 0 deletions src/actions/RecordsActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,35 @@ export function loadRecordsPhases() {
};
}

export function loadAllowedRejectReason() {
return function (dispatch) {
dispatch(loadAllowedRejectReasonPending());
return axiosBackend
.get(`${API_URL}/rest/records/allowedRejectReason`)
.then((response) => {
dispatch(loadAllowedRejectReasonSuccess(response.data));
})
.catch((error) => {
dispatch(loadAllowedRejectReasonError(error));
});
};
}

export function loadAllowedRejectReasonSuccess(isAllowedRejectReason) {
return {
type: ActionConstants.LOAD_ALLOWED_REJECT_REASON_SUCCESS,
isAllowedRejectReason,
};
}

export function loadAllowedRejectReasonError(error) {
return asyncError(ActionConstants.LOAD_ALLOWED_REJECT_REASON_ERROR, error);
}

export function loadAllowedRejectReasonPending() {
return asyncRequest(ActionConstants.LOAD_ALLOWED_REJECT_REASON_PENDING);
}

export function loadRecordsPhasesPending() {
return asyncRequest(ActionConstants.LOAD_RECORDS_PHASES_PENDING);
}
Expand Down
86 changes: 86 additions & 0 deletions src/components/button/RejectButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types"; // Import PropTypes
import { Button, Modal, Form } from "react-bootstrap";
import { useI18n } from "../../hooks/useI18n.jsx";
import { useDispatch, useSelector } from "react-redux";
import { loadAllowedRejectReason } from "../../actions/RecordsActions.js";
import "bootstrap/dist/css/bootstrap.min.css";

const RejectButton = ({
children,
className = "",
variant = "danger",
size = "sm",
disabled = true,
onClick = () => {},
}) => {
const { i18n } = useI18n();
const dispatch = useDispatch();
const isAllowedRejectReason = useSelector((state) => state.records.isAllowedRejectReason.data);
const [showModal, setShowModal] = useState(false);
const [rejectionReason, setRejectionReason] = useState("");

useEffect(() => {
dispatch(loadAllowedRejectReason());
}, [dispatch]);

const handleShow = () => setShowModal(true);
const handleClose = () => setShowModal(false);
const handleInputChange = (event) => setRejectionReason(event.target.value);
const handleReject = () => {
setShowModal(false);
onClick(rejectionReason);
};

return (
<>
{isAllowedRejectReason ? (
<Button className={className} size={size} disabled={disabled} variant={variant} onClick={handleShow}>
{children}
</Button>
) : (
<Button className={className} size={size} disabled={disabled} variant={variant} onClick={onClick}>
{children}
</Button>
)}

<Modal show={showModal} onHide={handleClose}>
<Modal.Header className="bg-warning">
<Modal.Title className="h5">{i18n("reject-dialog-title")}</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form>
<Form.Group controlId="formRejectionReason">
<Form.Control
as="textarea"
rows={4}
placeholder={i18n("records.rejection-reason-placeholder")}
value={rejectionReason}
onChange={handleInputChange}
/>
</Form.Group>
</Form>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleClose}>
{i18n("close")}
</Button>
<Button variant="danger" onClick={handleReject}>
{i18n("reject")}
</Button>
</Modal.Footer>
</Modal>
</>
);
};

RejectButton.propTypes = {
children: PropTypes.node.isRequired,
className: PropTypes.string,
variant: PropTypes.string,
size: PropTypes.string,
disabled: PropTypes.bool,
onClick: PropTypes.func,
};

export default RejectButton;
5 changes: 3 additions & 2 deletions src/components/record/Record.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { isAdmin } from "../../utils/SecurityUtils";
import PromiseTrackingMask from "../misc/PromiseTrackingMask";
import { Constants as SConstants, FormUtils } from "@kbss-cvut/s-forms";
import FormValidationDialog from "../FormValidationDialog.jsx";
import RejectButton from "../button/RejectButton.jsx";

class Record extends React.Component {
static propTypes = {
Expand Down Expand Up @@ -188,7 +189,7 @@ class Record extends React.Component {
{EXTENSIONS === EXTENSION_CONSTANTS.SUPPLIER &&
!record.isNew &&
(record.phase === RECORD_PHASE.OPEN || this._isAdmin()) && (
<Button
<RejectButton
className="mx-1 action-button"
variant="danger"
size="sm"
Expand All @@ -203,7 +204,7 @@ class Record extends React.Component {
>
{this.i18n("reject")}
{recordSaved.status === ACTION_STATUS.PENDING && <LoaderSmall />}
</Button>
</RejectButton>
)}

{(EXTENSIONS === EXTENSION_CONSTANTS.SUPPLIER || EXTENSIONS === EXTENSION_CONSTANTS.OPERATOR) &&
Expand Down
32 changes: 20 additions & 12 deletions src/components/record/RecordController.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,27 +130,35 @@ class RecordController extends React.Component {
};

_onComplete = () => {
this._handlePhaseChange(RECORD_PHASE.COMPLETED);
this._transitionToRecords();
this._handlePhaseChange(RECORD_PHASE.COMPLETED, () => {
this._handleRejectReason(null, () => {
this.props.updateRecord(this.state.record, this.props.currentUser);
this._transitionToRecords();
});
});
};

_onReject = () => {
this._handlePhaseChange(RECORD_PHASE.REJECTED);
this._transitionToRecords();
_onReject = (reason) => {
this._handlePhaseChange(RECORD_PHASE.REJECTED, () => {
this._handleRejectReason(reason, () => {
this.props.updateRecord(this.state.record, this.props.currentUser);
this._transitionToRecords();
});
});
};

_handlePhaseChange = (newPhase) => {
const currentUser = this.props.currentUser;
const update = { ...this.state.record, phase: newPhase };

this.setState({ record: update });
_handleRejectReason(reason, callback) {
const update = { ...this.state.record, rejectReason: reason };
this.setState({ record: update }, callback);
}

this.props.updateRecord(update, currentUser);
_handlePhaseChange = (newPhase, callback) => {
const update = { ...this.state.record, phase: newPhase };
this.setState({ record: update }, callback);
};

_getLocalName() {
if (EXTENSIONS.split(",").includes("kodi")) {
// return name of the record based on answer of specific question
return this._getKodiLocaLName();
}
return "record-" + Date.now();
Expand Down
13 changes: 10 additions & 3 deletions src/components/record/RecordRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ StatusInfo[RECORD_PHASE.REJECTED] = {
tooltip: "records.completion-status-tooltip.rejected",
};

let RecordRow = (props) => {
const RecordRow = (props) => {
const { i18n } = useI18n();
const record = props.record,
formTemplateOptions = props.formTemplateOptions,
Expand All @@ -37,6 +37,14 @@ let RecordRow = (props) => {
);
const statusInfo = StatusInfo[record.phase];

const statusInfoText = () => {
if (record.rejectReason) {
return `${i18n(statusInfo.tooltip)}\n${i18n("reason")}: ${record.rejectReason}`;
} else {
return `${i18n(statusInfo.tooltip)}`;
}
};

return (
<tr className="position-relative">
<IfGranted expected={ROLE.ADMIN} actual={props.currentUser.role}>
Expand All @@ -61,9 +69,8 @@ let RecordRow = (props) => {
{formatDate(new Date(record.lastModified ? record.lastModified : record.dateCreated))}
</td>
<td className="report-row content-center">
{statusInfo ? <HelpIcon text={i18n(statusInfo.tooltip)} glyph={statusInfo.glyph} /> : "N/A"}
{statusInfo ? <HelpIcon text={statusInfoText()} glyph={statusInfo.glyph} /> : "N/A"}
</td>

<td className="report-row actions">
<PromiseTrackingMask area={`record-${record.key}`} />
<Button variant="primary" size="sm" title={i18n("records.open-tooltip")} onClick={() => props.onEdit(record)}>
Expand Down
5 changes: 5 additions & 0 deletions src/constants/ActionConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,8 @@ export const DISMISS_MESSAGE = "DISMISS_MESSAGE";
export const LOAD_RECORDS_PHASES_PENDING = "LOAD_PHASES_PENDING";
export const LOAD_RECORDS_PHASES_SUCCESS = "LOAD_PHASES_SUCCESS";
export const LOAD_RECORDS_PHASES_ERROR = "LOAD_PHASES_ERROR";

// server configuration
export const LOAD_ALLOWED_REJECT_REASON_PENDING = "LOAD_ALLOWED_REJECT_REASON_PENDING";
export const LOAD_ALLOWED_REJECT_REASON_SUCCESS = "LOAD_ALLOWED_REJECT_REASON_SUCCESS";
export const LOAD_ALLOWED_REJECT_REASON_ERROR = "LOAD_ALLOWED_REJECT_REASON_ERROR";
5 changes: 4 additions & 1 deletion src/i18n/cs.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default {
reject: "Odmítnout",
complete: "Dokončit",
publish: "Publikovat",
reason: "Důvod",
"select.placeholder": "Vyberte...",

"login.title": Constants.APP_NAME + " - Přihlášení",
Expand Down Expand Up @@ -121,6 +122,8 @@ export default {
"user.impersonate": "Impersonace",
"user.impersonate-error": "Impersonace se nepodařila. {error}",
"user.edit": "Upravit profil",
"reject-dialog-title": "Proč chcete odmítnout?",
"records.rejection-reason-placeholder": "Zadejte důvod odmítnutí",

"institutions.panel-title": "Instituce",
"institutions.create-institution": "Vytvořit instituci",
Expand Down Expand Up @@ -155,7 +158,7 @@ export default {
"records.completion-status.rejected": "Odmítnutý",
"records.completion-status-tooltip.complete": "Všechny povinné informace ze záznamu byly vyplněny.",
"records.completion-status-tooltip.incomplete": "Některé povinné informace ze záznamu ještě nebyly vyplněny.",
"records.completion-status-tooltip.rejected": "Formulář byl odmítnut",
"records.completion-status-tooltip.rejected": "Formulář byl odmítnut.",
"records.completion-status-tooltip.published": "Formulář byl zveřejněn",
"records.last-modified": "Naposledy upraveno",
"records.open-tooltip": "Zobrazit či upravit tento záznam",
Expand Down
5 changes: 4 additions & 1 deletion src/i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default {
reject: "Reject",
complete: "Complete",
publish: "Publish",
reason: "Reason",
"select.placeholder": "Select...",

"login.title": Constants.APP_NAME + " - Login",
Expand Down Expand Up @@ -156,7 +157,7 @@ export default {
"records.completion-status-tooltip.complete": "All required fields of the record have been filled out.",
"records.completion-status-tooltip.incomplete":
"Some of the required fields of the record have not yet been filled out.",
"records.completion-status-tooltip.rejected": "The form was rejected",
"records.completion-status-tooltip.rejected": "The form was rejected.",
"records.completion-status-tooltip.published": "The form has been published",
"records.last-modified": "Last modified",
"records.open-tooltip": "View and edit the record",
Expand All @@ -178,6 +179,8 @@ export default {
"records.import.success.message":
"Successfully imported all {importedCount, plural, one {# record} other {# records}}.",
"records.import.error.message": "Record import failed. Check the server log for details.",
"reject-dialog-title": "Why do you want to reject?",
"records.rejection-reason-placeholder": "Enter your reason for rejection",

"record.panel-title": "Form {identifier}",
"record.form-title": "Details",
Expand Down
28 changes: 28 additions & 0 deletions src/reducers/RecordsReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ACTION_STATUS } from "../constants/DefaultConstants";
const initialState = {
recordsLoaded: {},
recordsPhases: {},
isAllowedRejectReason: {},
};

export default function (state = initialState, action) {
Expand Down Expand Up @@ -59,6 +60,33 @@ export default function (state = initialState, action) {
error: action.error,
},
};
case ActionConstants.LOAD_ALLOWED_REJECT_REASON_PENDING:
return {
...state,
isAllowedRejectReason: {
status: ACTION_STATUS.PENDING,
...state.isAllowedRejectReason,
},
};

case ActionConstants.LOAD_ALLOWED_REJECT_REASON_SUCCESS:
return {
...state,
isAllowedRejectReason: {
status: ACTION_STATUS.PENDING,
data: state.isAllowedRejectReason,
error: "",
},
};
case ActionConstants.LOAD_ALLOWED_REJECT_REASON_ERROR:
return {
...state,
isAllowedRejectReason: {
status: ACTION_STATUS.ERROR,
error: action.error,
},
};

default:
return state;
}
Expand Down

0 comments on commit 15be05f

Please sign in to comment.