From fe982cc03557ce5116988c4087f3f746752f8334 Mon Sep 17 00:00:00 2001 From: Tomasz Subik Date: Tue, 3 Dec 2024 14:08:20 +0100 Subject: [PATCH] confirm delete modal for removing documents and annexes --- .../documentation/documents-certification.js | 1 + components/ui/confirm-modal.js | 59 +++++++++++++++++++ components/ui/doc-annex.js | 7 +-- components/ui/doc-card-upload.js | 42 ++++++++----- components/ui/doc-card.js | 58 ++++++++++++++---- css/components/ui/_button.scss | 12 ++++ css/components/ui/_confirm-modal.scss | 19 ++++++ css/components/ui/_modal.scss | 11 ++++ css/index.scss | 1 + e2e/cypress/e2e/operator.cy.js | 7 +++ lang/zu.json | 4 ++ 11 files changed, 188 insertions(+), 33 deletions(-) create mode 100644 components/ui/confirm-modal.js create mode 100644 css/components/ui/_confirm-modal.scss diff --git a/components/operators-detail/documentation/documents-certification.js b/components/operators-detail/documentation/documents-certification.js index 30547552..a3a6629f 100644 --- a/components/operators-detail/documentation/documents-certification.js +++ b/components/operators-detail/documentation/documents-certification.js @@ -63,6 +63,7 @@ function DocumentsCertification(props) { user.operator_ids.includes(+id))) && ( { + const intl = useIntl(); + const [submitting, setSubmitting] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const _cancelText = cancelText || intl.formatMessage({ id: 'cancel', defaultMessage: 'Cancel' }); + const _confirmText = confirmText || intl.formatMessage({ id: 'confirm', defaultMessage: 'Confirm' }); + + const handleConfirm = () => { + setSubmitting(true); + onConfirm({ + onSuccess: () => { + setErrorMessage(null); + }, + onError: (errorMessage) => { + setSubmitting(false); + setErrorMessage(errorMessage); + } + }); + }; + + return ( +
+ + + {title &&

{title}

} +

+ {text} +

+

+ {errorMessage} +

+
+ + +
+
+ ) +}; + +export default ConfirmModal; diff --git a/components/ui/doc-annex.js b/components/ui/doc-annex.js index e0b91227..f7d33913 100644 --- a/components/ui/doc-annex.js +++ b/components/ui/doc-annex.js @@ -1,13 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -// import classnames from 'classnames'; import Tooltip from 'rc-tooltip'; import { useIntl } from 'react-intl'; import Icon from 'components/ui/icon'; -import Spinner from 'components/ui/spinner'; -export default function DocAnnex({ annex, isRemoving, showRemoveButton, onRemove }) { +export default function DocAnnex({ annex, showRemoveButton, visible, onRemove }) { const intl = useIntl(); const formatDate = (date) => intl.formatDate(date, { day: '2-digit', @@ -18,12 +16,12 @@ export default function DocAnnex({ annex, isRemoving, showRemoveButton, onRemove return ( -

{annex.name}

{intl.formatMessage({ id: 'annex.start_date' })}:
@@ -68,6 +66,5 @@ DocAnnex.defaultProps = { DocAnnex.propTypes = { annex: PropTypes.object.isRequired, showRemoveButton: PropTypes.bool, - isRemoving: PropTypes.bool, onRemove: PropTypes.func } diff --git a/components/ui/doc-card-upload.js b/components/ui/doc-card-upload.js index c8252228..09f0052c 100644 --- a/components/ui/doc-card-upload.js +++ b/components/ui/doc-card-upload.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import dayjs from 'dayjs'; +import * as Sentry from '@sentry/nextjs'; import { connect } from 'react-redux'; @@ -15,18 +16,13 @@ import DocumentationService from 'services/documentationService'; import modal from 'services/modal'; // Components +import ConfirmModal from 'components/ui/confirm-modal'; import DocModal from 'components/ui/doc-modal'; -import Spinner from 'components/ui/spinner'; class DocCardUpload extends React.Component { constructor(props) { super(props); - // STATE - this.state = { - deleteLoading: false, - }; - // BINDINGS this.triggerAddFile = this.triggerAddFile.bind(this); this.triggerEditFile = this.triggerEditFile.bind(this); @@ -92,24 +88,43 @@ class DocCardUpload extends React.Component { triggerDeleteFile(e) { e && e.preventDefault(); - const { docId } = this.props; + const { title, intl } = this.props; + + modal.toggleModal(true, { + children: ConfirmModal, + childrenProps: { + title: intl.formatMessage({ id: 'delete.document.title', defaultMessage: 'Delete {document}?' }, { document: title }), + text: intl.formatMessage( + { id: 'delete.document.text', defaultMessage: 'Are you sure you want to delete document {document}?' }, { document: title } + ), + confirmText: intl.formatMessage({ id: 'delete', defaultMessage: 'Delete' }), + onConfirm: this.triggerConfirmedDeleteFile.bind(this), + onCancel: () => modal.toggleModal(false), + }, + size: '-small' + }); + } - this.setState({ deleteLoading: true }); + triggerConfirmedDeleteFile({ onSuccess, onError } = {}) { + const { docId, intl } = this.props; this.documentationService.deleteDocument(docId) .then(() => { - this.setState({ deleteLoading: false }); + modal.toggleModal(false); + onSuccess && onSuccess(); this.props.onChange && this.props.onChange(); }) .catch((err) => { - this.setState({ deleteLoading: false }); + onError && onError( + intl.formatMessage({ id: 'document.delete.error', defaultMessage: 'An error occurred while deleting the document.' }) + ); + Sentry.captureException(err); console.error(err); }); } render() { const { status, buttons, date } = this.props; - const { deleteLoading } = this.state; const currentDate = dayjs(new Date()); const selectedDate = dayjs(date); const isEditable = @@ -153,10 +168,6 @@ class DocCardUpload extends React.Component { className="c-button -small -primary" > {this.props.intl.formatMessage({ id: 'delete' })} - )} @@ -215,6 +226,7 @@ class DocCardUpload extends React.Component { DocCardUpload.propTypes = { status: PropTypes.string, + title: PropTypes.string, user: PropTypes.object, docId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), onChange: PropTypes.func, diff --git a/components/ui/doc-card.js b/components/ui/doc-card.js index ae475fa1..a1664dd8 100644 --- a/components/ui/doc-card.js +++ b/components/ui/doc-card.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; +import * as Sentry from '@sentry/nextjs'; // Intl import { connect } from 'react-redux'; @@ -11,6 +12,7 @@ import modal from 'services/modal'; import DocumentationService from 'services/documentationService'; // Components +import ConfirmModal from 'components/ui/confirm-modal'; import DocAnnexesModal from 'components/ui/doc-annexes-modal'; import DocAnnex from 'components/ui/doc-annex'; import Icon from 'components/ui/icon'; @@ -47,16 +49,15 @@ class DocCard extends React.Component { constructor(props) { super(props); + this.state = { + annexTooltipVisible: undefined + }; + this.documentationService = new DocumentationService({ authorization: props.user.token }); } - state = { - deleteLoading: false - } - - triggerWhy = (e) => { e && e.preventDefault(); const { title, reason } = this.props; @@ -125,15 +126,46 @@ class DocCard extends React.Component { } triggerRemoveAnnex = (id) => { - this.setState({ deleteLoading: true }); + const { annexes, title, intl } = this.props; + const annex = annexes.find(a => a.id === id); + + // workaround to close tooltip before opening modal, but show it again when hovering + this.setState({ annexTooltipVisible: false }); + setTimeout(() => { + this.setState({ annexTooltipVisible: undefined }); + }); + + modal.toggleModal(true, { + children: ConfirmModal, + childrenProps: { + title: intl.formatMessage({ id: 'delete.document.title', defaultMessage: 'Delete {document}?' }, { document: annex.name }), + text: intl.formatMessage( + { id: 'delete.document.text', defaultMessage: 'Are you sure you want to delete document {document}?' }, { document: annex.name } + ), + confirmText: intl.formatMessage({ id: 'delete', defaultMessage: 'Delete' }), + onConfirm: (options) => { + this.triggerConfirmedRemoveAnnex({ ...options, annexId: id }); + }, + onCancel: () => modal.toggleModal(false), + }, + size: '-small' + }); + } + + triggerConfirmedRemoveAnnex({ annexId, onSuccess, onError } = {}) { + const { intl } = this.props; - this.documentationService.deleteAnnex(id) + this.documentationService.deleteAnnex(annexId) .then(() => { - this.setState({ deleteLoading: false }); + modal.toggleModal(false); + onSuccess && onSuccess(); this.props.onChange && this.props.onChange(); }) .catch((err) => { - this.setState({ deleteLoading: false }); + onError && onError( + intl.formatMessage({ id: 'document.delete.error', defaultMessage: 'An error occurred while deleting the document.' }) + ); + Sentry.captureException(err); console.error(err); }); } @@ -141,7 +173,7 @@ class DocCard extends React.Component { render() { const { user, adminComment, public: publicState, startDate, endDate, status, source, sourceInfo, title, explanation, url, annexes, layout, properties } = this.props; const { id } = properties; - const { deleteLoading } = this.state; + const { annexTooltipVisible } = this.state; const isActiveUser = (user && user.role === 'admin') || (user && (user.role === 'operator' || user.role === 'holding') && user.operator_ids && user.operator_ids.includes(+id)) || (user && user.role === 'government' && user.country && user.country.toString() === id); @@ -223,7 +255,7 @@ class DocCard extends React.Component {
    {approvedAnnexes.map(annex => (
  • - +
  • ))} {isActiveUser && @@ -296,7 +328,7 @@ class DocCard extends React.Component {
      {approvedAnnexes.map(annex => (
    • - +
    • ))} {isActiveUser && @@ -363,7 +395,7 @@ class DocCard extends React.Component {
        {approvedAnnexes.map(annex => (
      • - +
      • ))} diff --git a/css/components/ui/_button.scss b/css/components/ui/_button.scss index 6035cd45..463365ce 100644 --- a/css/components/ui/_button.scss +++ b/css/components/ui/_button.scss @@ -67,6 +67,18 @@ } } + &.-dangerous { + border-color: $color-error; + color: $color-text-2; + fill: $color-error; + background: $color-error; + + &:hover { + color: darken($color-text-2, 10%); + fill: darken($color-text-2, 10%); + } + } + &.-tertiary { border-color: $color-tertiary; color: $color-text-1; diff --git a/css/components/ui/_confirm-modal.scss b/css/components/ui/_confirm-modal.scss new file mode 100644 index 00000000..3b2ba4cc --- /dev/null +++ b/css/components/ui/_confirm-modal.scss @@ -0,0 +1,19 @@ +.c-confirm-modal { + padding: 20px; + + p { + font-size: $font-size-big; + } + + p.-error { + color: $color-error; + font-size: $font-size-default; + } + + .actions { + display: flex; + margin-top: 30px; + gap: 20px; + justify-content: flex-end; + } +} diff --git a/css/components/ui/_modal.scss b/css/components/ui/_modal.scss index 351a1d71..54d128d0 100644 --- a/css/components/ui/_modal.scss +++ b/css/components/ui/_modal.scss @@ -113,6 +113,17 @@ } } + &.-small { + .modal-container { + width: calc(100% - 20px); + + @include breakpoint(medium) { + width: unset; + max-width: 600px; + } + } + } + &.-auto { .modal-container { width: unset; diff --git a/css/index.scss b/css/index.scss index 6c11aa54..27858a6d 100644 --- a/css/index.scss +++ b/css/index.scss @@ -56,6 +56,7 @@ @import 'components/ui/chart'; @import 'components/ui/chart-legend'; @import 'components/ui/country-card'; +@import 'components/ui/confirm-modal'; @import 'components/ui/datepicker'; @import 'components/ui/doc-by-category'; @import 'components/ui/doc-card'; diff --git a/e2e/cypress/e2e/operator.cy.js b/e2e/cypress/e2e/operator.cy.js index 000f77dc..bc8f9432 100644 --- a/e2e/cypress/e2e/operator.cy.js +++ b/e2e/cypress/e2e/operator.cy.js @@ -85,6 +85,10 @@ describe('Operator', function () { .siblings('.c-doc-card-upload') .contains('button', 'Delete') .click(); + + cy.contains('Are you sure you want to delete document').should('be.visible'); + cy.get('[data-test-id=confirm-modal-confirm]').click(); + cy.wait('@documentsReload'); cy.wait(1000); @@ -201,6 +205,9 @@ describe('Operator', function () { cy.get('[data-test-id=remove-annex-button]') .click(); + cy.contains('Are you sure you want to delete document').should('be.visible'); + cy.get('[data-test-id=confirm-modal-confirm]').click(); + cy.docGetProducerDocCard('Arrêté d’agrément du personnel du centre socio-sanitaire de l’entreprise') .find('.doc-card-annexes .doc-card-list-item') .should('have.length', 0) diff --git a/lang/zu.json b/lang/zu.json index 1dba1f31..1b815e7c 100644 --- a/lang/zu.json +++ b/lang/zu.json @@ -135,8 +135,12 @@ "notrequired-file": "Non applicable", "delete": "Delete", "cancel": "Cancel", + "confirm": "Confirm", "submit": "Submit", "send": "Send", + "document.delete.title": "Delete {document}?", + "document.delete.text": "Are you sure you want to delete document {document}?", + "document.delete.error": "An error occurred while deleting the document.", "high": "High", "medium": "Medium", "low": "Low",