General-purpose "Confirm" modal

Added a re-usable "yes/no" confirmation modal where the client just sets the question string and gets a callback "OK" or "Cancel" is clicked.

It doesn't make sense to create one modal for each confirmation, especially when the modal is only used in one place.

Replaced one of the existing modal as an example.
This commit is contained in:
infinite-persistence 2021-09-11 23:02:34 +08:00
parent 4bfb4fb55d
commit 11bbd58e33
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
8 changed files with 84 additions and 67 deletions

View file

@ -2,7 +2,7 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import WalletSwap from './view'; import WalletSwap from './view';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import { doAddCoinSwap, doQueryCoinSwapStatus } from 'redux/actions/coinSwap'; import { doAddCoinSwap, doRemoveCoinSwap, doQueryCoinSwapStatus } from 'redux/actions/coinSwap';
import { doToast } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import { selectCoinSwaps } from 'redux/selectors/coinSwap'; import { selectCoinSwaps } from 'redux/selectors/coinSwap';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
@ -19,6 +19,7 @@ const perform = (dispatch) => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
doToast: (options) => dispatch(doToast(options)), doToast: (options) => dispatch(doToast(options)),
addCoinSwap: (coinSwap) => dispatch(doAddCoinSwap(coinSwap)), addCoinSwap: (coinSwap) => dispatch(doAddCoinSwap(coinSwap)),
removeCoinSwap: (chargeCode) => dispatch(doRemoveCoinSwap(chargeCode)),
getNewAddress: () => dispatch(doGetNewAddress()), getNewAddress: () => dispatch(doGetNewAddress()),
checkAddressIsMine: (address) => dispatch(doCheckAddressIsMine(address)), checkAddressIsMine: (address) => dispatch(doCheckAddressIsMine(address)),
queryCoinSwapStatus: (sendAddress) => dispatch(doQueryCoinSwapStatus(sendAddress)), queryCoinSwapStatus: (sendAddress) => dispatch(doQueryCoinSwapStatus(sendAddress)),

View file

@ -59,6 +59,7 @@ type Props = {
isAuthenticated: boolean, isAuthenticated: boolean,
doToast: ({ message: string }) => void, doToast: ({ message: string }) => void,
addCoinSwap: (CoinSwapInfo) => void, addCoinSwap: (CoinSwapInfo) => void,
removeCoinSwap: (string) => void,
getNewAddress: () => void, getNewAddress: () => void,
checkAddressIsMine: (string) => void, checkAddressIsMine: (string) => void,
openModal: (string, {}) => void, openModal: (string, {}) => void,
@ -72,6 +73,7 @@ function WalletSwap(props: Props) {
coinSwaps, coinSwaps,
isAuthenticated, isAuthenticated,
addCoinSwap, addCoinSwap,
removeCoinSwap,
getNewAddress, getNewAddress,
checkAddressIsMine, checkAddressIsMine,
openModal, openModal,
@ -103,9 +105,15 @@ function WalletSwap(props: Props) {
setSwap(null); setSwap(null);
} }
function removeCoinSwap(chargeCode) { function handleRemoveSwap(chargeCode) {
openModal(MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS, { openModal(MODALS.CONFIRM, {
chargeCode: chargeCode, title: __('Remove Swap'),
subtitle: <I18nMessage tokens={{ address: <em>{`${chargeCode}`}</em> }}>Remove %address%?</I18nMessage>,
body: <p className="help--warning">{__('This process cannot be reversed.')}</p>,
onConfirm: (closeModal) => {
removeCoinSwap(chargeCode);
closeModal();
},
}); });
} }
@ -657,7 +665,7 @@ function WalletSwap(props: Props) {
button="link" button="link"
icon={ICONS.REMOVE} icon={ICONS.REMOVE}
title={__('Remove swap')} title={__('Remove swap')}
onClick={() => removeCoinSwap(x.chargeCode)} onClick={() => handleRemoveSwap(x.chargeCode)}
/> />
</td> </td>
</tr> </tr>

View file

@ -1,3 +1,4 @@
export const CONFIRM = 'confirm';
export const CONFIRM_FILE_REMOVE = 'confirm_file_remove'; export const CONFIRM_FILE_REMOVE = 'confirm_file_remove';
export const CONFIRM_EXTERNAL_RESOURCE = 'confirm_external_resource'; export const CONFIRM_EXTERNAL_RESOURCE = 'confirm_external_resource';
export const COMMENT_ACKNOWEDGEMENT = 'comment_acknowlegement'; export const COMMENT_ACKNOWEDGEMENT = 'comment_acknowlegement';
@ -42,7 +43,6 @@ export const SYNC_ENABLE = 'SYNC_ENABLE';
export const IMAGE_UPLOAD = 'image_upload'; export const IMAGE_UPLOAD = 'image_upload';
export const MOBILE_SEARCH = 'mobile_search'; export const MOBILE_SEARCH = 'mobile_search';
export const VIEW_IMAGE = 'view_image'; export const VIEW_IMAGE = 'view_image';
export const CONFIRM_REMOVE_BTC_SWAP_ADDRESS = 'confirm_remove_btc_swap_address';
export const BLOCK_CHANNEL = 'block_channel'; export const BLOCK_CHANNEL = 'block_channel';
export const COLLECTION_ADD = 'collection_add'; export const COLLECTION_ADD = 'collection_add';
export const COLLECTION_DELETE = 'collection_delete'; export const COLLECTION_DELETE = 'collection_delete';

View file

@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import ModalConfirm from './view';
const perform = (dispatch) => ({
doHideModal: () => dispatch(doHideModal()),
});
export default connect(null, perform)(ModalConfirm);

View file

@ -0,0 +1,56 @@
// @flow
import React from 'react';
import type { Node } from 'react';
import Button from 'component/button';
import Card from 'component/common/card';
import Spinner from 'component/spinner';
import { Modal } from 'modal/modal';
type Props = {
title: string,
subtitle?: string | Node,
body?: string | Node,
labelOk?: string,
labelCancel?: string,
onConfirm: (closeModal: () => void, setIsBusy: (boolean) => void) => void,
hideCancel?: boolean,
// --- perform ---
doHideModal: () => void,
};
export default function ModalConfirm(props: Props) {
const { title, subtitle, body, labelOk, labelCancel, onConfirm, hideCancel, doHideModal } = props;
const [isBusy, setIsBusy] = React.useState(false);
function handleOnClick() {
if (onConfirm) {
onConfirm(doHideModal, setIsBusy);
}
}
function getOkLabel() {
return isBusy ? <Spinner type="small" /> : labelOk || __('OK');
}
function getCancelLabel() {
return labelCancel || __('Cancel');
}
return (
<Modal isOpen type="card" onAborted={doHideModal}>
<Card
title={title}
subtitle={subtitle}
body={body}
actions={
<>
<div className="section__actions">
<Button button="primary" label={getOkLabel()} disabled={isBusy} onClick={handleOnClick} />
{!hideCancel && <Button button="link" label={getCancelLabel()} disabled={isBusy} onClick={doHideModal} />}
</div>
</>
}
/>
</Modal>
);
}

View file

@ -1,13 +0,0 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import ModalRemoveBtcSwapAddress from './view';
import { doRemoveCoinSwap } from 'redux/actions/coinSwap';
const select = (state, props) => ({});
const perform = (dispatch) => ({
removeCoinSwap: (chargeCode) => dispatch(doRemoveCoinSwap(chargeCode)),
closeModal: () => dispatch(doHideModal()),
});
export default connect(select, perform)(ModalRemoveBtcSwapAddress);

View file

@ -1,43 +0,0 @@
// @flow
import React from 'react';
import { Modal } from 'modal/modal';
import Button from 'component/button';
import Card from 'component/common/card';
import I18nMessage from 'component/i18nMessage';
type Props = {
chargeCode: string,
removeCoinSwap: (string) => void,
closeModal: () => void,
};
function ModalRemoveBtcSwapAddress(props: Props) {
const { chargeCode, removeCoinSwap, closeModal } = props;
return (
<Modal isOpen contentLabel={__('Confirm Swap Removal')} type="card" onAborted={closeModal}>
<Card
title={__('Remove Swap')}
subtitle={<I18nMessage tokens={{ address: <em>{`${chargeCode}`}</em> }}>Remove %address%?</I18nMessage>}
body={<p className="help--warning">{__('This process cannot be reversed.')}</p>}
actions={
<>
<div className="section__actions">
<Button
button="primary"
label={__('OK')}
onClick={() => {
removeCoinSwap(chargeCode);
closeModal();
}}
/>
<Button button="link" label={__('Cancel')} onClick={closeModal} />
</div>
</>
}
/>
</Modal>
);
}
export default ModalRemoveBtcSwapAddress;

View file

@ -23,6 +23,7 @@ const ModalClaimCollectionAdd = lazyImport(() =>
const ModalCommentAcknowledgement = lazyImport(() => const ModalCommentAcknowledgement = lazyImport(() =>
import('modal/modalCommentAcknowledgement' /* webpackChunkName: "modalCommentAcknowledgement" */) import('modal/modalCommentAcknowledgement' /* webpackChunkName: "modalCommentAcknowledgement" */)
); );
const ModalConfirm = lazyImport(() => import('modal/modalConfirm' /* webpackChunkName: "modalConfirm" */));
const ModalConfirmAge = lazyImport(() => import('modal/modalConfirmAge' /* webpackChunkName: "modalConfirmAge" */)); const ModalConfirmAge = lazyImport(() => import('modal/modalConfirmAge' /* webpackChunkName: "modalConfirmAge" */));
const ModalConfirmThumbnailUpload = lazyImport(() => const ModalConfirmThumbnailUpload = lazyImport(() =>
import('modal/modalConfirmThumbnailUpload' /* webpackChunkName: "modalConfirmThumbnailUpload" */) import('modal/modalConfirmThumbnailUpload' /* webpackChunkName: "modalConfirmThumbnailUpload" */)
@ -63,9 +64,6 @@ const ModalPublish = lazyImport(() => import('modal/modalPublish' /* webpackChun
const ModalPublishPreview = lazyImport(() => const ModalPublishPreview = lazyImport(() =>
import('modal/modalPublishPreview' /* webpackChunkName: "modalPublishPreview" */) import('modal/modalPublishPreview' /* webpackChunkName: "modalPublishPreview" */)
); );
const ModalRemoveBtcSwapAddress = lazyImport(() =>
import('modal/modalRemoveBtcSwapAddress' /* webpackChunkName: "modalRemoveBtcSwapAddress" */)
);
const ModalRemoveCard = lazyImport(() => import('modal/modalRemoveCard' /* webpackChunkName: "modalRemoveCard" */)); const ModalRemoveCard = lazyImport(() => import('modal/modalRemoveCard' /* webpackChunkName: "modalRemoveCard" */));
const ModalRemoveComment = lazyImport(() => const ModalRemoveComment = lazyImport(() =>
import('modal/modalRemoveComment' /* webpackChunkName: "modalRemoveComment" */) import('modal/modalRemoveComment' /* webpackChunkName: "modalRemoveComment" */)
@ -124,6 +122,8 @@ function ModalRouter(props: Props) {
function getModal(id) { function getModal(id) {
switch (id) { switch (id) {
case MODALS.CONFIRM:
return ModalConfirm;
case MODALS.UPGRADE: case MODALS.UPGRADE:
return ModalUpgrade; return ModalUpgrade;
case MODALS.DOWNLOADING: case MODALS.DOWNLOADING:
@ -198,8 +198,6 @@ function ModalRouter(props: Props) {
return ModalViewImage; return ModalViewImage;
case MODALS.MASS_TIP_UNLOCK: case MODALS.MASS_TIP_UNLOCK:
return ModalMassTipsUnlock; return ModalMassTipsUnlock;
case MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS:
return ModalRemoveBtcSwapAddress;
case MODALS.BLOCK_CHANNEL: case MODALS.BLOCK_CHANNEL:
return ModalBlockChannel; return ModalBlockChannel;
case MODALS.COLLECTION_ADD: case MODALS.COLLECTION_ADD: