// Copyright 2021 99cloud // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import React, { Component } from 'react'; import { inject, observer } from 'mobx-react'; import { Modal, Button, Tooltip } from 'antd'; import { isArray, isFunction, isBoolean } from 'lodash'; import Confirm from 'components/Confirm'; import PropTypes from 'prop-types'; import Notify from 'components/Notify'; import classnames from 'classnames'; import { firstUpperCase, allSettled } from 'utils'; import styles from './index.less'; function getDefaultMsg(action, data) { const { actionName, title } = action; const name = isArray(data) ? data.map((it) => it.name).join(', ') : data.name; const submitErrorMsg = t('Unable to {action} {name}.', { action: actionName.toLowerCase() || title, name, }); const performErrorMsg = t('You are not allowed to { action } {name}.', { action: actionName.toLowerCase() || title, name, }); const submitSuccessMsg = firstUpperCase( t('{action} {name} successfully.', { action: actionName.toLowerCase() || title, name, }) ); const confirmContext = t('Are you sure to { action } {name}?', { action: actionName.toLowerCase() || title, name, }); return { submitErrorMsg, submitSuccessMsg, confirmContext, performErrorMsg, }; } @inject('rootStore') @observer class ActionButton extends Component { static propTypes() { return { title: PropTypes.string.isRequired, id: PropTypes.string.isRequired, perform: PropTypes.func.isRequired, item: PropTypes.object, actionType: PropTypes.string, icon: PropTypes.string, isAllowed: PropTypes.bool, needHide: PropTypes.bool, buttonType: PropTypes.string, items: PropTypes.array, isBatch: PropTypes.bool, path: PropTypes.string, onFinishAction: PropTypes.func, action: PropTypes.any, containerProps: PropTypes.any, maxLength: PropTypes.number, isFirstAction: PropTypes.bool, onClickAction: PropTypes.func, visible: PropTypes.bool, }; } static defaultProps = { item: undefined, isAllowed: false, confirm: false, needHide: true, buttonType: 'link', isLink: false, items: [], isBatch: false, path: '', containerProps: {}, maxLength: 0, isFirstAction: false, onClickAction: null, visible: false, }; constructor(props) { super(props); const { id } = props; if (!id) { throw Error('need id!'); } this.state = { visible: false, submitLoading: false, }; } get routing() { return this.props.rootStore.routing; } onClick = () => { const { actionType, onClickAction } = this.props; switch (actionType) { case 'confirm': this.onShowConfirm(); break; case 'link': { const { action, item, containerProps } = this.props; const { path } = action; if (isFunction(path)) { const newPath = path(item, containerProps); this.routing.push(newPath); } else { this.routing.push(path); } break; } default: this.formRef = React.createRef(); this.showModalAction(); } if (onClickAction) { onClickAction(); } }; handleSubmitLoading = (flag) => { this.setState({ submitLoading: !!flag, }); }; handleSubmit = (values) => { const { item, isBatch, items } = this.props; if (!this.onSubmit) return; this.handleSubmitLoading(true); const data = isBatch ? items : item; const result = this.onSubmit(values, data); if (result instanceof Promise) { result .then( () => { this.onShowSuccess(data); }, (error) => { this.onShowError(data, error); } ) .finally(() => { this.handleSubmitLoading(); }); } else { this.handleSubmitLoading(); if (result) { this.onShowSuccess(data); } else { this.onShowError(data, result); } } }; onOK = () => { const { onSubmit, form, item, isBatch, items } = this.props; if (!onSubmit) return; this.handleSubmitLoading(true); form.validateFields([], (err, values) => { if (err) { // eslint-disable-next-line no-console console.log('Values has error: ', err); return; } // eslint-disable-next-line no-console console.log('Received values of form: ', values); const data = isBatch ? items : item; const result = onSubmit(form.getFieldsValue(), data); if (result instanceof Promise) { result .then( () => { this.onShowSuccess(data); }, (error) => { this.onShowError(data, error); } ) .finally(() => { this.handleSubmitLoading(); }); } else { this.handleSubmitLoading(); if (result) { this.onShowSuccess(data); } else { this.onShowError(data, result); } } }); }; onShowSuccess = (data, afterSubmit) => { const { submitSuccessMsg } = this.props.action; const message = submitSuccessMsg ? submitSuccessMsg(data) : getDefaultMsg(this.props.action, data).submitSuccessMsg; Notify.success(message); this.onCallback(true, false, afterSubmit); }; // eslint-disable-next-line no-shadow onCallback = (success, fail, afterSubmit) => { const { onFinishAction, id } = this.props; if (onFinishAction) { const isDelete = id === 'delete'; setTimeout(() => { onFinishAction(success, fail, isDelete, afterSubmit); }, 500); } }; // eslint-disable-next-line no-unused-vars onShowError = (data, error) => { // this.handleModalVisible(); const { showConfirmErrorBeforeSubmit, confirmErrorMessageBeforeSubmit } = this.props.action; if (showConfirmErrorBeforeSubmit) { Confirm.error({ content: confirmErrorMessageBeforeSubmit, }); this.onCallback(false, true); return; } const { submitErrorMsg } = this.props.action; const { data: responseData } = (error || {}).response || error || {}; const realError = responseData || error; const message = submitErrorMsg ? submitErrorMsg(data, realError) : getDefaultMsg(this.props.action, data).submitErrorMsg; Notify.errorWithDetail(realError, message); this.onCallback(false, true); }; onShowConfirm = async () => { const { perform, title, confirmContext, okText, cancelText, onSubmit, afterSubmit, } = this.props.action; const { item, items, isBatch, containerProps, onCancelAction } = this.props; const data = isBatch ? items : item; const content = confirmContext ? confirmContext(data) : getDefaultMsg(this.props.action, data).confirmContext; try { perform(data).then( () => { Confirm.confirm({ title, content, okText, cancelText, onOk: () => this.onConfirmOK( data, onSubmit, isBatch, containerProps, afterSubmit ), onCancel: () => { onCancelAction && onCancelAction(); }, }); }, (error) => { const message = error || getDefaultMsg(this.props.action, data).performErrorMsg; Confirm.error({ content: message, }); } ); } catch (error) { // eslint-disable-next-line no-console console.log(error); const message = error || getDefaultMsg(this.props.action, data).performErrorMsg; Confirm.error({ content: message, }); } }; onSubmitOne = (data, onSubmit, containerProps, afterSubmit) => new Promise((resolve, reject) => { const result = onSubmit(data, containerProps); if (result instanceof Promise) { result.then( () => { this.onShowSuccess(data, afterSubmit); resolve(); }, (error) => { reject(error); } ); } else if (result) { this.onShowSuccess(data, afterSubmit); resolve(); } else { reject(result); } }).catch((error) => { this.onShowError(data, error); }); onSubmitBatch = (data, onSubmit, containerProps, isBatch, afterSubmit) => new Promise((resolve, reject) => { const promises = data.map((it, index) => onSubmit(it, containerProps, isBatch, index, data) ); const results = allSettled(promises); results.then((res) => { const failedDatas = res .map((it, idx) => { if (it.status === 'rejected') { return { data: data[idx], reason: it.reason, }; } return null; }) .filter((it) => !!it); if (failedDatas.length === 0) { this.onShowSuccess(data, afterSubmit); return resolve(); } failedDatas.forEach((it) => { this.onShowError(it.data, it.reason); }); if (failedDatas.length === data.length) { return reject(); } return resolve(); }); }); onConfirmOK = (data, onSubmit, isBatch, containerProps, afterSubmit) => { if (isBatch) { return this.onSubmitBatch( data, onSubmit, containerProps, isBatch, afterSubmit ); } return this.onSubmitOne(data, onSubmit, containerProps, afterSubmit); }; onClickModalActionOk = () => { const { containerProps } = this.props; return this.formRef.current.wrappedInstance.onClickSubmit( (success, fail) => { this.handleSubmitLoading(); this.onClickModalActionCancel(true); this.onCallback(success, fail); }, () => { this.handleSubmitLoading(true); }, containerProps ); }; onClickModalActionCancel = (finish) => { if (!isBoolean(finish)) { this.formRef.current.wrappedInstance.onClickCancel(); } const { onCancelAction } = this.props; this.setState( { visible: false, }, () => { onCancelAction && onCancelAction(); } ); }; getModalWidth = (size) => { switch (size) { case 'small': return 520; case 'middle': return 720; case 'large': return 1200; default: return 520; } }; showModalAction() { this.setState({ visible: true, }); } renderModal() { const { visible, submitLoading } = this.state; if (!visible) { return null; } const { title, action, item, containerProps, items } = this.props; const ActionComponent = action; const { modalSize, okText, cancelText, id, className, readOnly } = action; const width = this.getModalWidth(modalSize); const modalProps = { title, visible, className: classnames(`modal-${id}`, styles['modal-action'], className), width, onOk: () => this.onClickModalActionOk(), onCancel: this.onClickModalActionCancel, confirmLoading: submitLoading, okText, cancelText, }; if (readOnly) { modalProps.cancelButtonProps = { style: { display: 'none' }, }; } return ( ); } render() { const { isAllowed, needHide, buttonType, buttonClassName, name, id, title, danger, style, maxLength, isFirstAction, } = this.props; if (!isAllowed && needHide) { return null; } const buttonText = name || title; let showTip = false; if (isFirstAction && buttonText && buttonText.length > maxLength) { showTip = true; } const button = ( ); const buttonRender = showTip ? ( {button} ) : ( button ); return ( <> {buttonRender} {this.renderModal()} ); } } export default ActionButton;