Skip to content

Commit

Permalink
Adding notification action button to exercise-related pages.
Browse files Browse the repository at this point in the history
  • Loading branch information
krulis-martin committed Sep 15, 2023
1 parent 32e5ff2 commit 992ec01
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 3 deletions.
135 changes: 135 additions & 0 deletions src/components/Exercises/ExerciseButtons/ExerciseButtons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Row, Col, Modal, FormGroup, FormControl, FormLabel, Overlay, Popover } from 'react-bootstrap';

import Button, { TheButtonGroup } from '../../widgets/TheButton';
import InsetPanel from '../../widgets/InsetPanel';
import { BellIcon, CloseIcon, LoadingIcon, SendIcon, WarningIcon } from '../../icons';

const ExerciseButtons = ({ id, archivedAt = null, permissionHints = null, sendNotification }) => {
const [message, setMessage] = useState(null); // null = dialog is hidden, string = message (and dialog is open)
const [sendResult, setSendResult] = useState(null); // null = initial, true = pending, false = error, int = result
// the sendResult holds, how many users were notified (-1 is server error)
const buttonTarget = useRef(null);

return !archivedAt && permissionHints && permissionHints.update ? (
<>
<Row>
<Col className="mb-3" xs={12} lg={true}>
<TheButtonGroup></TheButtonGroup>
</Col>

<Col xs={12} lg="auto" className="mb-3">
<TheButtonGroup className="text-nowrap">
<Button
variant={
sendResult === false || sendResult < 0
? 'danger'
: sendResult === null || sendResult === true
? 'warning'
: 'success'
}
onClick={() => (sendResult !== null && sendResult !== true ? setSendResult(null) : setMessage(''))}
disabled={sendResult !== null}
ref={buttonTarget}>
{sendResult === true ? <LoadingIcon gapRight /> : <BellIcon gapRight />}
<FormattedMessage id="app.exercise.notificationButton" defaultMessage="Send Notification" />
</Button>
</TheButtonGroup>

<Overlay target={buttonTarget.current} show={sendResult !== null && sendResult !== true} placement="bottom">
{props => (
<Popover id={id} {...props}>
<Popover.Title>
{sendResult === false || sendResult < 0 ? (
<>
<WarningIcon className="text-danger" gapRight />
<FormattedMessage
id="app.exercise.notificationButton.failedMessage"
defaultMessage="The operation has failed!"
/>
</>
) : sendResult === 0 ? (
<FormattedMessage
id="app.exercise.notificationButton.noRecipients"
defaultMessage="No recipients of the notification were found. Please note that the users may choose to ignore these notifications in their personal settings."
/>
) : (
<FormattedMessage
id="app.exercise.notificationButton.successMessage"
defaultMessage="The notification was successfully sent to {sendResult} {sendResult, plural, one {user} other {users}}."
values={{ sendResult }}
/>
)}
</Popover.Title>
<Popover.Content className="text-center">
<Button onClick={() => setSendResult(null)} size="xs" variant="success">
<FormattedMessage id="generic.acknowledge" defaultMessage="Acknowledge" />
</Button>
</Popover.Content>
</Popover>
)}
</Overlay>
</Col>
</Row>

<Modal show={message !== null} backdrop="static" onHide={() => setMessage(null)} size="xl">
<Modal.Header closeButton>
<Modal.Title>
<FormattedMessage
id="app.exercise.notificationModal.title"
defaultMessage="Send a notification to teachers"
/>
</Modal.Title>
</Modal.Header>

<Modal.Body>
<InsetPanel>
<FormattedMessage
id="app.exercise.notificationModal.explain"
defaultMessage="A notification is sent by email to all group admins and supervisors who have assigned this exercise in their groups. Optionally, you may attach a custom message to the notification. If you leave the message empty, a generic notification informing that the exercise was changed will be sent."
/>
</InsetPanel>

<FormGroup controlId="message">
<FormLabel>
<FormattedMessage id="generic.message" defaultMessage="Message" />:
</FormLabel>
<FormControl type="text" value={message || ''} onChange={ev => setMessage(ev.target.value)} />
</FormGroup>
</Modal.Body>

<Modal.Footer className="d-block text-center">
<TheButtonGroup className="text-nowrap">
<Button
variant="success"
onClick={() => {
if (sendResult !== true) {
setSendResult(true);
sendNotification(message).then(({ value }) => setSendResult(value));
setMessage(null);
}
}}>
<SendIcon gapRight />
<FormattedMessage id="generic.send" defaultMessage="Send" />
</Button>
<Button variant="secondary" onClick={() => setMessage(null)}>
<CloseIcon gapRight />
<FormattedMessage id="generic.close" defaultMessage="Close" />
</Button>
</TheButtonGroup>
</Modal.Footer>
</Modal>
</>
) : null;
};

ExerciseButtons.propTypes = {
id: PropTypes.string.isRequired,
archivedAt: PropTypes.number,
permissionHints: PropTypes.object,
sendNotification: PropTypes.func.isRequired,
};

export default ExerciseButtons;
2 changes: 2 additions & 0 deletions src/components/Exercises/ExerciseButtons/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import ExerciseButtons from './ExerciseButtons';
export default ExerciseButtons;
1 change: 1 addition & 0 deletions src/components/icons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const AssignmentIcon = props => <Icon {...props} icon="laptop-code" />;
export const AssignmentsIcon = props => <Icon {...props} icon="tasks" />;
export const AuthorIcon = props => <Icon {...props} icon="user-pen" />;
export const BanIcon = props => <Icon {...props} icon="ban" />;
export const BellIcon = props => <Icon {...props} icon={['far', 'bell']} />;
export const BindIcon = props => <Icon {...props} icon="link" />;
export const BonusIcon = props => <Icon {...props} icon="hand-holding-usd" />;
export const BugIcon = props => <Icon {...props} icon="bug" />;
Expand Down
8 changes: 8 additions & 0 deletions src/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,12 @@
"app.exercise.noRefSolutions": "Úloha nebyla dostatečně ověřena. Aby bylo možné úlohu zadat musí existovat alespoň jedno referenční řešení.",
"app.exercise.noReferenceSolutions": "Tato úloha zatím nemá žádná referenční řešení.",
"app.exercise.noReferenceSolutionsDetailed": "Konfigurace úlohy by měla být prověřena alespoň jedním referenčním řešením než bude možné ji zadat ve skupině.",
"app.exercise.notificationButton": "Poslat oznámení",
"app.exercise.notificationButton.failedMessage": "Tato operace selhala!",
"app.exercise.notificationButton.noRecipients": "Odeslané oznámení nemá žádné platné příjemce. Vezměte prosím na vědomí, že uživatelé si mohli tyto notifikace vypnout ve svém osobním nastavení.",
"app.exercise.notificationButton.successMessage": "Oznámení bylo úspěšně odesláno {sendResult} {sendResult, plural, one {uživateli} other {uživatelům}}.",
"app.exercise.notificationModal.explain": "Oznámení je odesláno emailem všem administrátorům a vedoucím skupin, ve kterých je tato úloha zadaná. Volitelně můžete doplnit vlastní zprávu s upřesnění oznámení. Pokud zprávu nevyplníte, bude odesláno obecné oznámení ohlašující změnu v úloze.",
"app.exercise.notificationModal.title": "Odeslat oznámení vyučujícím",
"app.exercise.referenceSolution.deleteConfirm": "Opravdu chcete smazat toto referenční řešení? Tuto akci není možné vrátit.",
"app.exercise.referenceSolutionsBox": "Propagovaná refereční řešení",
"app.exercise.runtimes": "Běhová prostředí",
Expand Down Expand Up @@ -1994,6 +2000,7 @@
"generic.lastUpdatedAt": "aktualizováno",
"generic.load": "Načíst",
"generic.loading": "Načítání...",
"generic.message": "Zpráva",
"generic.name": "Název",
"generic.nameOfPerson": "Jméno",
"generic.noRecordsInTable": "V tabulce nejsou žádné záznamy.",
Expand All @@ -2015,6 +2022,7 @@
"generic.scheduledAt": "Naplánováno",
"generic.search": "Vyhledat",
"generic.selectAll": "Vybrat vše",
"generic.send": "Odeslat",
"generic.setFilters": "Nastavit filtry",
"generic.settings": "Nastavení",
"generic.showAll": "Ukázat vše",
Expand Down
8 changes: 8 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,12 @@
"app.exercise.noRefSolutions": "Exercise has no proof of concept. Exercise must get at least one reference solution before it can be assigned.",
"app.exercise.noReferenceSolutions": "There are no reference solutions for this exercise yet.",
"app.exercise.noReferenceSolutionsDetailed": "The exercise configuration should be verified on one reference solution at least before it can be assigned.",
"app.exercise.notificationButton": "Send Notification",
"app.exercise.notificationButton.failedMessage": "The operation has failed!",
"app.exercise.notificationButton.noRecipients": "No recipients of the notification were found. Please note that the users may choose to ignore these notifications in their personal settings.",
"app.exercise.notificationButton.successMessage": "The notification was successfully sent to {sendResult} {sendResult, plural, one {user} other {users}}.",
"app.exercise.notificationModal.explain": "A notification is sent by email to all group admins and supervisors who have assigned this exercise in their groups. Optionally, you may attach a custom message to the notification. If you leave the message empty, a generic notification informing that the exercise was changed will be sent.",
"app.exercise.notificationModal.title": "Send a notification to teachers",
"app.exercise.referenceSolution.deleteConfirm": "Are you sure you want to delete the reference solution? This cannot be undone.",
"app.exercise.referenceSolutionsBox": "Promoted Reference Solutions",
"app.exercise.runtimes": "Runtime environments",
Expand Down Expand Up @@ -1994,6 +2000,7 @@
"generic.lastUpdatedAt": "updated",
"generic.load": "Load",
"generic.loading": "Loading...",
"generic.message": "Message",
"generic.name": "Name",
"generic.nameOfPerson": "Name",
"generic.noRecordsInTable": "There are no records in the table.",
Expand All @@ -2015,6 +2022,7 @@
"generic.scheduledAt": "Scheduled at",
"generic.search": "Search",
"generic.selectAll": "Select All",
"generic.send": "Send",
"generic.setFilters": "Set Filters",
"generic.settings": "Settings",
"generic.showAll": "Show All",
Expand Down
7 changes: 7 additions & 0 deletions src/pages/EditExercise/EditExercise.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import ExercisesTagsEditContainer from '../../containers/ExercisesTagsEditContai
import DeleteExerciseButtonContainer from '../../containers/DeleteExerciseButtonContainer';
import ArchiveExerciseButtonContainer from '../../containers/ArchiveExerciseButtonContainer';
import ExerciseCallouts, { exerciseCalloutsAreVisible } from '../../components/Exercises/ExerciseCallouts';
import ExerciseButtons from '../../components/Exercises/ExerciseButtons';
import EditExerciseUsers from '../../components/Exercises/EditExerciseUsers';
import { EditExerciseIcon } from '../../components/icons';

Expand All @@ -26,6 +27,7 @@ import {
fetchTags,
attachExerciseToGroup,
detachExerciseFromGroup,
sendNotification,
} from '../../redux/modules/exercises';
import { fetchAllGroups } from '../../redux/modules/groups';
import { fetchByIds } from '../../redux/modules/users';
Expand Down Expand Up @@ -114,6 +116,7 @@ class EditExercise extends Component {
detachingGroupId,
attachExerciseToGroup,
detachExerciseFromGroup,
sendNotification,
} = this.props;

return (
Expand All @@ -135,6 +138,8 @@ class EditExercise extends Component {
</Row>
)}

<ExerciseButtons {...exercise} sendNotification={sendNotification} />

{exercise.permissionHints.update && (
<Row>
<Col lg={6}>
Expand Down Expand Up @@ -240,6 +245,7 @@ EditExercise.propTypes = {
editExercise: PropTypes.func.isRequired,
attachExerciseToGroup: PropTypes.func.isRequired,
detachExerciseFromGroup: PropTypes.func.isRequired,
sendNotification: PropTypes.func.isRequired,
params: PropTypes.shape({
exerciseId: PropTypes.string.isRequired,
}).isRequired,
Expand All @@ -262,6 +268,7 @@ export default withLinks(
editExercise: (version, data) => dispatch(editExercise(exerciseId, { ...data, version })),
attachExerciseToGroup: groupId => dispatch(attachExerciseToGroup(exerciseId, groupId)),
detachExerciseFromGroup: groupId => dispatch(detachExerciseFromGroup(exerciseId, groupId)),
sendNotification: message => dispatch(sendNotification(exerciseId, message)),
})
)(EditExercise)
);
14 changes: 13 additions & 1 deletion src/pages/EditExerciseConfig/EditExerciseConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,18 @@ import EditEnvironmentSimpleForm from '../../components/forms/EditEnvironmentSim
import EditEnvironmentConfigForm from '../../components/forms/EditEnvironmentConfigForm';
import EditExercisePipelinesForm from '../../components/forms/EditExercisePipelinesForm/EditExercisePipelinesForm';
import ExerciseCallouts, { exerciseCalloutsAreVisible } from '../../components/Exercises/ExerciseCallouts';
import ExerciseButtons from '../../components/Exercises/ExerciseButtons';
import ExerciseConfigTypeButton from '../../components/buttons/ExerciseConfigTypeButton';
import { InfoIcon, TestsIcon } from '../../components/icons';
import Callout from '../../components/widgets/Callout';

import { fetchExercise, fetchExerciseIfNeeded, editExercise, invalidateExercise } from '../../redux/modules/exercises';
import {
fetchExercise,
fetchExerciseIfNeeded,
editExercise,
invalidateExercise,
sendNotification,
} from '../../redux/modules/exercises';
import {
fetchExerciseConfig,
fetchExerciseConfigIfNeeded,
Expand Down Expand Up @@ -274,6 +281,7 @@ class EditExerciseConfig extends Component {
pipelinesVariables,
supplementaryFiles,
supplementaryFilesStatus,
sendNotification,
} = this.props;

return (
Expand All @@ -300,6 +308,8 @@ class EditExerciseConfig extends Component {
</Row>
)}

<ExerciseButtons {...exercise} sendNotification={sendNotification} />

{hasPermissions(exercise, 'update') && isEmpoweredSupervisorRole(effectiveRole) && (
<table className="em-margin-vertical">
<tbody>
Expand Down Expand Up @@ -595,6 +605,7 @@ EditExerciseConfig.propTypes = {
reloadExercise: PropTypes.func.isRequired,
reloadConfig: PropTypes.func.isRequired,
invalidateExercise: PropTypes.func.isRequired,
sendNotification: PropTypes.func.isRequired,
navigate: withRouterProps.navigate,
location: withRouterProps.location,
params: PropTypes.shape({ exerciseId: PropTypes.string }).isRequired,
Expand Down Expand Up @@ -639,6 +650,7 @@ export default withRouter(
])
),
invalidateExercise: () => dispatch(invalidateExercise(exerciseId)),
sendNotification: message => dispatch(sendNotification(exerciseId, message)),
})
)(injectIntl(EditExerciseConfig))
)
Expand Down
7 changes: 7 additions & 0 deletions src/pages/EditExerciseLimits/EditExerciseLimits.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import HardwareGroupMetadata from '../../components/Exercises/HardwareGroupMetad
import EditHardwareGroupForm from '../../components/forms/EditHardwareGroupForm';
import EditLimitsForm from '../../components/forms/EditLimitsForm/EditLimitsForm';
import ExerciseCallouts, { exerciseCalloutsAreVisible } from '../../components/Exercises/ExerciseCallouts';
import ExerciseButtons from '../../components/Exercises/ExerciseButtons';
import ResourceRenderer from '../../components/helpers/ResourceRenderer';
import Icon, { LimitsIcon } from '../../components/icons';
import Callout from '../../components/widgets/Callout';
Expand All @@ -22,6 +23,7 @@ import {
fetchExerciseIfNeeded,
setExerciseHardwareGroups,
invalidateExercise,
sendNotification,
} from '../../redux/modules/exercises';
import {
fetchExerciseLimits,
Expand Down Expand Up @@ -124,6 +126,7 @@ class EditExerciseLimits extends Component {
cloneHorizontally,
cloneVertically,
cloneAll,
sendNotification,
} = this.props;

return (
Expand All @@ -145,6 +148,8 @@ class EditExerciseLimits extends Component {
</Row>
)}

<ExerciseButtons {...exercise} sendNotification={sendNotification} />

{Boolean(exercise.hardwareGroups && exercise.hardwareGroups.length > 1) && (
<Row>
<Col sm={12}>
Expand Down Expand Up @@ -290,6 +295,7 @@ EditExerciseLimits.propTypes = {
cloneAll: PropTypes.func.isRequired,
reloadExercise: PropTypes.func.isRequired,
invalidateExercise: PropTypes.func.isRequired,
sendNotification: PropTypes.func.isRequired,
};

const cloneVerticallyWrapper = defaultMemoize(
Expand Down Expand Up @@ -333,5 +339,6 @@ export default connect(
fetchExerciseLimits: envId => dispatch(fetchExerciseLimits(exerciseId, envId)),
reloadExercise: () => dispatch(fetchExercise(exerciseId)),
invalidateExercise: () => dispatch(invalidateExercise(exerciseId)),
sendNotification: message => dispatch(sendNotification(exerciseId, message)),
})
)(EditExerciseLimits);
Loading

0 comments on commit 992ec01

Please sign in to comment.