-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactoring solution actions (buttons) into one component (container)…
… which works for top-level buttons as well as inline button-dropdown.
- Loading branch information
1 parent
2ca4c66
commit 8b3a184
Showing
20 changed files
with
353 additions
and
301 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
|
||
import Button from '../../widgets/TheButton'; | ||
import OptionalTooltipWrapper from '../../widgets/OptionalTooltipWrapper'; | ||
import Icon, { LoadingIcon } from '../../icons'; | ||
|
||
const ActionButton = ({ | ||
id, | ||
variant = 'success', | ||
icon, | ||
label, | ||
shortLabel = label, | ||
confirm, | ||
pending, | ||
captionAsTooltip, | ||
size, | ||
onClick, | ||
}) => | ||
confirm ? ( | ||
<Button variant={variant} size={size} onClick={onClick} disabled={pending} confirm={confirm} confirmId={id}> | ||
{pending ? <LoadingIcon gapRight={!captionAsTooltip} /> : <Icon icon={icon} gapRight={!captionAsTooltip} />} | ||
{!captionAsTooltip && label} | ||
</Button> | ||
) : ( | ||
<OptionalTooltipWrapper tooltip={label} hide={!captionAsTooltip}> | ||
<Button variant={variant} size={size} onClick={onClick} disabled={pending}> | ||
{pending ? <LoadingIcon gapRight={!captionAsTooltip} /> : <Icon icon={icon} gapRight={!captionAsTooltip} />} | ||
{!captionAsTooltip && shortLabel} | ||
</Button> | ||
</OptionalTooltipWrapper> | ||
); | ||
|
||
ActionButton.propTypes = { | ||
id: PropTypes.string.isRequired, | ||
variant: PropTypes.string, | ||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, | ||
shortLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), | ||
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.array]).isRequired, | ||
confirm: PropTypes.string, | ||
pending: PropTypes.bool, | ||
captionAsTooltip: PropTypes.bool, | ||
size: PropTypes.string, | ||
onClick: PropTypes.func.isRequired, | ||
}; | ||
|
||
export default ActionButton; |
77 changes: 77 additions & 0 deletions
77
src/components/Solutions/SolutionActions/ActionDropdown.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import React, { useState, useRef } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { FormattedMessage } from 'react-intl'; | ||
import { Dropdown, Overlay, Popover } from 'react-bootstrap'; | ||
|
||
import Button, { TheButtonGroup } from '../../widgets/TheButton'; | ||
import Icon, { CloseIcon, EditIcon, LoadingIcon, SuccessIcon } from '../../icons'; | ||
|
||
const ActionDropdown = ({ actions, placement = 'bottom', id, captionAsTooltip }) => { | ||
const [confirmAction, setConfirmAction] = useState(null); | ||
const target = useRef(null); | ||
|
||
return ( | ||
<Dropdown as="span"> | ||
<Dropdown.Toggle variant="warning" size="xs" ref={target}> | ||
<EditIcon gapRight={!captionAsTooltip} /> | ||
{!captionAsTooltip && <FormattedMessage id="generic.update" defaultMessage="Update" />} | ||
</Dropdown.Toggle> | ||
|
||
<Dropdown.Menu> | ||
{actions.map(action => ( | ||
<Dropdown.Item | ||
key={action.icon} | ||
onClick={action.confirm ? () => setConfirmAction(action) : action.handler} | ||
disabled={action.pending}> | ||
<small> | ||
{action.pending ? ( | ||
<LoadingIcon gapRight /> | ||
) : ( | ||
<Icon icon={action.icon} className={`text-${action.variant || 'success'}`} gapRight /> | ||
)} | ||
{action.label} | ||
</small> | ||
</Dropdown.Item> | ||
))} | ||
</Dropdown.Menu> | ||
|
||
<Overlay target={target} placement={placement} show={target !== null && confirmAction !== null}> | ||
<Popover id={id}> | ||
<Popover.Title>{confirmAction && confirmAction.confirm}</Popover.Title> | ||
<Popover.Content className="text-center"> | ||
<TheButtonGroup> | ||
<Button | ||
onClick={() => { | ||
confirmAction.handler(); | ||
setConfirmAction(null); | ||
}} | ||
size="sm" | ||
variant="success"> | ||
<SuccessIcon gapRight /> | ||
<FormattedMessage id="app.confirm.yes" defaultMessage="Yes" /> | ||
</Button> | ||
<Button | ||
onClick={() => { | ||
setConfirmAction(null); | ||
}} | ||
size="sm" | ||
variant="danger"> | ||
<CloseIcon gapRight /> | ||
<FormattedMessage id="app.confirm.no" defaultMessage="No" /> | ||
</Button> | ||
</TheButtonGroup> | ||
</Popover.Content> | ||
</Popover> | ||
</Overlay> | ||
</Dropdown> | ||
); | ||
}; | ||
|
||
ActionDropdown.propTypes = { | ||
actions: PropTypes.array.isRequired, | ||
placement: PropTypes.string, | ||
id: PropTypes.string.isRequired, | ||
captionAsTooltip: PropTypes.bool, | ||
}; | ||
|
||
export default ActionDropdown; |
168 changes: 168 additions & 0 deletions
168
src/components/Solutions/SolutionActions/SolutionActions.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { FormattedMessage } from 'react-intl'; | ||
import { withRouter } from 'react-router'; | ||
|
||
import withLinks from '../../../helpers/withLinks'; | ||
|
||
import ActionButton from './ActionButton'; | ||
import ActionDropdown from './ActionDropdown'; | ||
|
||
/** | ||
* Action templates containing basic parameters: label, short (label), icon (name), variant (success if missing), | ||
* and confirm (confirm yes/no message for a popover; no confirmation required if missing) | ||
*/ | ||
const actionsTemplates = { | ||
accept: { | ||
short: <FormattedMessage id="app.acceptSolution.notAcceptedShort" defaultMessage="Accept" />, | ||
label: <FormattedMessage id="app.acceptSolution.notAccepted" defaultMessage="Accept as Final" />, | ||
icon: ['far', 'check-circle'], | ||
pending: 'acceptPending', | ||
}, | ||
unaccept: { | ||
short: <FormattedMessage id="app.acceptSolution.acceptedShort" defaultMessage="Revoke" />, | ||
label: <FormattedMessage id="app.acceptSolution.accepted" defaultMessage="Revoke as Final" />, | ||
icon: ['far', 'circle-xmark'], | ||
variant: 'warning', | ||
pending: 'acceptPending', | ||
}, | ||
|
||
// review actions | ||
open: { | ||
label: <FormattedMessage id="app.reviewSolutionButtons.open" defaultMessage="Start Review" />, | ||
icon: 'microscope', | ||
variant: 'info', | ||
}, | ||
reopen: { | ||
label: <FormattedMessage id="app.reviewSolutionButtons.reopen" defaultMessage="Reopen Review" />, | ||
icon: 'person-digging', | ||
variant: 'warning', | ||
}, | ||
openClose: { | ||
label: <FormattedMessage id="app.reviewSolutionButtons.markReviewed" defaultMessage="Mark as Reviewed" />, | ||
icon: 'file-circle-check', | ||
}, | ||
close: { | ||
label: <FormattedMessage id="app.reviewSolutionButtons.close" defaultMessage="Close Review" />, | ||
icon: 'boxes-packing', | ||
}, | ||
delete: { | ||
label: <FormattedMessage id="app.reviewSolutionButtons.delete" defaultMessage="Erase Review" />, | ||
icon: 'trash', | ||
variant: 'danger', | ||
confirm: ( | ||
<FormattedMessage | ||
id="app.reviewSolutionButtons.deleteConfirm" | ||
defaultMessage="All review comments will be erased as well. Do you wish to proceed?" | ||
/> | ||
), | ||
}, | ||
}; | ||
|
||
const knownActions = ['accept', 'unaccept', 'open', 'reopen', 'openClose', 'close', 'delete']; | ||
|
||
const SolutionActions = ({ | ||
id, | ||
solution, | ||
acceptPending = false, | ||
showAllButtons = false, | ||
updatePending = false, | ||
captionAsTooltip = false, | ||
size = undefined, | ||
dropdown = false, | ||
setAccepted = null, | ||
setReviewState = null, | ||
deleteReview = null, | ||
history: { push }, | ||
location: { pathname }, | ||
links: { SOLUTION_SOURCE_CODES_URI_FACTORY }, | ||
}) => { | ||
const review = solution && solution.review; | ||
const assignmentId = solution && solution.assignmentId; | ||
const accepted = solution && solution.accepted; | ||
const permissionHints = solution && solution.permissionHints; | ||
if (!permissionHints.setFlag) { | ||
setAccepted = null; | ||
} | ||
if (!permissionHints.review) { | ||
setReviewState = deleteReview = null; | ||
} | ||
|
||
const reviewPageUri = SOLUTION_SOURCE_CODES_URI_FACTORY(assignmentId, id); | ||
const isOnReviewPage = pathname === SOLUTION_SOURCE_CODES_URI_FACTORY(assignmentId, id); | ||
|
||
const actionHandlers = { | ||
accept: !accepted && setAccepted && (() => setAccepted(true)), | ||
unaccept: accepted && setAccepted && (() => setAccepted(false)), | ||
open: setReviewState && (!review || !review.startedAt) && (() => setReviewState(false)), | ||
reopen: setReviewState && review && review.closedAt && (() => setReviewState(false)), | ||
openClose: setReviewState && (!review || !review.startedAt) && showAllButtons && (() => setReviewState(true)), | ||
close: setReviewState && review && review.startedAt && !review.closedAt && (() => setReviewState(true)), | ||
delete: showAllButtons && review && review.startedAt && deleteReview, | ||
}; | ||
|
||
if (!isOnReviewPage) { | ||
actionHandlers.open = actionHandlers.open && (() => actionHandlers.open().then(() => push(reviewPageUri))); | ||
actionHandlers.reopen = actionHandlers.reopen && (() => actionHandlers.reopen().then(() => push(reviewPageUri))); | ||
} | ||
|
||
const actions = knownActions | ||
.filter(a => actionHandlers[a]) | ||
.map(a => ({ | ||
...actionsTemplates[a], | ||
handler: actionHandlers[a], | ||
pending: actionsTemplates[a].pending === 'acceptPending' ? acceptPending : updatePending, | ||
})); | ||
|
||
return dropdown ? ( | ||
<ActionDropdown id={id} actions={actions} captionAsTooltip={captionAsTooltip} /> | ||
) : ( | ||
actions.map(action => ( | ||
<ActionButton | ||
key={action.icon} | ||
id={id} | ||
variant={action.variant} | ||
icon={action.icon} | ||
label={action.label} | ||
shortLabel={action.short || action.label} | ||
confirm={action.confirm} | ||
pending={action.pending} | ||
captionAsTooltip={captionAsTooltip} | ||
size={size} | ||
onClick={action.handler} | ||
/> | ||
)) | ||
); | ||
}; | ||
|
||
SolutionActions.propTypes = { | ||
id: PropTypes.string.isRequired, | ||
solution: PropTypes.shape({ | ||
accepted: PropTypes.bool, | ||
assignmentId: PropTypes.string.isRequired, | ||
review: PropTypes.shape({ | ||
startedAt: PropTypes.number, | ||
closedAt: PropTypes.number, | ||
issues: PropTypes.number, | ||
}), | ||
permissionHints: PropTypes.object, | ||
}), | ||
showAllButtons: PropTypes.bool, | ||
acceptPending: PropTypes.bool, | ||
updatePending: PropTypes.bool, | ||
captionAsTooltip: PropTypes.bool, | ||
size: PropTypes.string, | ||
dropdown: PropTypes.bool, | ||
setAccepted: PropTypes.func, | ||
setReviewState: PropTypes.func, | ||
deleteReview: PropTypes.func, | ||
history: PropTypes.shape({ | ||
push: PropTypes.func.isRequired, | ||
}), | ||
location: PropTypes.shape({ | ||
pathname: PropTypes.string.isRequired, | ||
}).isRequired, | ||
links: PropTypes.object.isRequired, | ||
}; | ||
|
||
export default withLinks(withRouter(SolutionActions)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import SolutionActions from './SolutionActions'; | ||
export default SolutionActions; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.