PLT-6987 User access token UI (#7007)
* Add user access token UI * Fix enter press and update mattermost-redux * Updating UI for access token stuff (#7066) * Revert segment key
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
4ef844298f
Коммит
5da5c0bbfb
@@ -3,20 +3,30 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {getMe} from 'mattermost-redux/actions/users';
|
||||
import {getMe, getUserAccessTokensForUser, createUserAccessToken, revokeUserAccessToken, clearUserAccessTokens} from 'mattermost-redux/actions/users';
|
||||
import * as UserUtils from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import SecurityTab from './user_settings_security.jsx';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const tokensEnabled = state.entities.general.config.EnableUserAccessTokens === 'true';
|
||||
const userHasTokenRole = UserUtils.hasUserAccessTokenRole(ownProps.user.roles) || UserUtils.isSystemAdmin(ownProps.user.roles);
|
||||
|
||||
return {
|
||||
...ownProps
|
||||
...ownProps,
|
||||
userAccessTokens: state.entities.users.myUserAccessTokens,
|
||||
canUseAccessTokens: tokensEnabled && userHasTokenRole
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getMe
|
||||
getMe,
|
||||
getUserAccessTokensForUser,
|
||||
createUserAccessToken,
|
||||
revokeUserAccessToken,
|
||||
clearUserAccessTokens
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import SettingItemMax from 'components/setting_item_max.jsx';
|
||||
import AccessHistoryModal from 'components/access_history_modal';
|
||||
import ActivityLogModal from 'components/activity_log_modal';
|
||||
import ToggleModalButton from 'components/toggle_modal_button.jsx';
|
||||
import ConfirmModal from 'components/confirm_modal.jsx';
|
||||
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
|
||||
@@ -13,15 +14,22 @@ import * as Utils from 'utils/utils.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {updatePassword, getAuthorizedApps, deactivateMfa, deauthorizeOAuthApp} from 'actions/user_actions.jsx';
|
||||
import {trackEvent} from 'actions/diagnostics_actions.jsx';
|
||||
import {isMobile} from 'utils/user_agent.jsx';
|
||||
|
||||
import $ from 'jquery';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl';
|
||||
import * as UserUtils from 'mattermost-redux/utils/user_utils';
|
||||
import {FormattedMessage, FormattedTime, FormattedDate, FormattedHTMLMessage} from 'react-intl';
|
||||
import {browserHistory, Link} from 'react-router/es6';
|
||||
|
||||
import icon50 from 'images/icon50x50.png';
|
||||
|
||||
const TOKEN_CREATING = 'creating';
|
||||
const TOKEN_CREATED = 'created';
|
||||
const TOKEN_NOT_CREATING = 'not_creating';
|
||||
|
||||
export default class SecurityTab extends React.Component {
|
||||
static propTypes = {
|
||||
user: PropTypes.object,
|
||||
@@ -31,26 +39,45 @@ export default class SecurityTab extends React.Component {
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
collapseModal: PropTypes.func.isRequired,
|
||||
setEnforceFocus: PropTypes.func.isRequired,
|
||||
|
||||
/*
|
||||
* The user access tokens for the user
|
||||
*/
|
||||
userAccessTokens: PropTypes.object,
|
||||
|
||||
/*
|
||||
* Set if access tokens are enabled and this user can use them
|
||||
*/
|
||||
canUseAccessTokens: PropTypes.bool,
|
||||
|
||||
actions: PropTypes.shape({
|
||||
getMe: PropTypes.func.isRequired
|
||||
getMe: PropTypes.func.isRequired,
|
||||
|
||||
/*
|
||||
* Function to get user access tokens for a user
|
||||
*/
|
||||
getUserAccessTokensForUser: PropTypes.func.isRequired,
|
||||
|
||||
/*
|
||||
* Function to create a user access token
|
||||
*/
|
||||
createUserAccessToken: PropTypes.func.isRequired,
|
||||
|
||||
/*
|
||||
* Function to revoke a user access token
|
||||
*/
|
||||
revokeUserAccessToken: PropTypes.func.isRequired,
|
||||
|
||||
/*
|
||||
* Function to clear user access tokens locally
|
||||
*/
|
||||
clearUserAccessTokens: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.submitPassword = this.submitPassword.bind(this);
|
||||
this.setupMfa = this.setupMfa.bind(this);
|
||||
this.removeMfa = this.removeMfa.bind(this);
|
||||
this.updateCurrentPassword = this.updateCurrentPassword.bind(this);
|
||||
this.updateNewPassword = this.updateNewPassword.bind(this);
|
||||
this.updateConfirmPassword = this.updateConfirmPassword.bind(this);
|
||||
this.getDefaultState = this.getDefaultState.bind(this);
|
||||
this.createPasswordSection = this.createPasswordSection.bind(this);
|
||||
this.createSignInSection = this.createSignInSection.bind(this);
|
||||
this.createOAuthAppsSection = this.createOAuthAppsSection.bind(this);
|
||||
this.deauthorizeApp = this.deauthorizeApp.bind(this);
|
||||
|
||||
this.state = this.getDefaultState();
|
||||
}
|
||||
|
||||
@@ -61,6 +88,8 @@ export default class SecurityTab extends React.Component {
|
||||
confirmPassword: '',
|
||||
passwordError: '',
|
||||
serverError: '',
|
||||
tokenError: '',
|
||||
showConfirmModal: false,
|
||||
authService: this.props.user.auth_service
|
||||
};
|
||||
}
|
||||
@@ -73,11 +102,18 @@ export default class SecurityTab extends React.Component {
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message}); //eslint-disable-line react/no-did-mount-set-state
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.canUseAccessTokens) {
|
||||
this.props.actions.clearUserAccessTokens();
|
||||
const userId = this.props.user ? this.props.user.id : '';
|
||||
this.props.actions.getUserAccessTokensForUser(userId, 0, 200);
|
||||
}
|
||||
}
|
||||
|
||||
submitPassword(e) {
|
||||
submitPassword = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
var user = this.props.user;
|
||||
@@ -127,12 +163,12 @@ export default class SecurityTab extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
setupMfa(e) {
|
||||
setupMfa = (e) => {
|
||||
e.preventDefault();
|
||||
browserHistory.push('/mfa/setup');
|
||||
}
|
||||
|
||||
removeMfa() {
|
||||
removeMfa = () => {
|
||||
deactivateMfa(
|
||||
() => {
|
||||
if (global.window.mm_license.MFA === 'true' &&
|
||||
@@ -157,19 +193,19 @@ export default class SecurityTab extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
updateCurrentPassword(e) {
|
||||
updateCurrentPassword = (e) => {
|
||||
this.setState({currentPassword: e.target.value});
|
||||
}
|
||||
|
||||
updateNewPassword(e) {
|
||||
updateNewPassword = (e) => {
|
||||
this.setState({newPassword: e.target.value});
|
||||
}
|
||||
|
||||
updateConfirmPassword(e) {
|
||||
updateConfirmPassword = (e) => {
|
||||
this.setState({confirmPassword: e.target.value});
|
||||
}
|
||||
|
||||
deauthorizeApp(e) {
|
||||
deauthorizeApp = (e) => {
|
||||
e.preventDefault();
|
||||
const appId = e.currentTarget.getAttribute('data-app');
|
||||
deauthorizeOAuthApp(
|
||||
@@ -183,10 +219,11 @@ export default class SecurityTab extends React.Component {
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createMfaSection() {
|
||||
createMfaSection = () => {
|
||||
let updateSectionStatus;
|
||||
let submit;
|
||||
|
||||
@@ -321,7 +358,7 @@ export default class SecurityTab extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
createPasswordSection() {
|
||||
createPasswordSection = () => {
|
||||
let updateSectionStatus;
|
||||
|
||||
if (this.props.activeSection === 'password') {
|
||||
@@ -578,7 +615,7 @@ export default class SecurityTab extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
createSignInSection() {
|
||||
createSignInSection = () => {
|
||||
let updateSectionStatus;
|
||||
const user = this.props.user;
|
||||
|
||||
@@ -793,7 +830,7 @@ export default class SecurityTab extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
createOAuthAppsSection() {
|
||||
createOAuthAppsSection = () => {
|
||||
let updateSectionStatus;
|
||||
|
||||
if (this.props.activeSection === 'apps') {
|
||||
@@ -929,6 +966,368 @@ export default class SecurityTab extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
startCreatingToken = () => {
|
||||
this.setState({tokenCreationState: TOKEN_CREATING});
|
||||
}
|
||||
|
||||
stopCreatingToken = () => {
|
||||
this.setState({tokenCreationState: TOKEN_NOT_CREATING});
|
||||
}
|
||||
|
||||
handleCreateToken = async () => {
|
||||
this.handleCancelConfirm();
|
||||
|
||||
const description = this.refs.newtokendescription ? this.refs.newtokendescription.value : '';
|
||||
|
||||
if (description === '') {
|
||||
this.setState({tokenError: Utils.localizeMessage('user.settings.tokens.nameRequired', 'Please enter a name.')});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({tokenError: ''});
|
||||
|
||||
const userId = this.props.user ? this.props.user.id : '';
|
||||
const {data, error} = await this.props.actions.createUserAccessToken(userId, description);
|
||||
|
||||
if (data) {
|
||||
this.setState({tokenCreationState: TOKEN_CREATED, newToken: data});
|
||||
} else if (error) {
|
||||
this.setState({serverError: error.message});
|
||||
}
|
||||
}
|
||||
|
||||
handleCancelConfirm = () => {
|
||||
this.setState({
|
||||
showConfirmModal: false,
|
||||
confirmTitle: null,
|
||||
confirmMessage: null,
|
||||
confirmButton: null,
|
||||
confirmComplete: null
|
||||
});
|
||||
}
|
||||
|
||||
confirmCreateToken = () => {
|
||||
if (UserUtils.isSystemAdmin(this.props.user.roles)) {
|
||||
this.setState({
|
||||
showConfirmModal: true,
|
||||
confirmTitle: (
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.confirmCreateTitle'
|
||||
defaultMessage='Create System Admin User Access Token'
|
||||
/>
|
||||
),
|
||||
confirmMessage: (
|
||||
<div className='alert alert-danger'>
|
||||
<FormattedHTMLMessage
|
||||
id='user.settings.tokens.confirmCreateMessage'
|
||||
defaultMessage='You are generating a user access token with System Admin permissions. Are you sure want to create this token?'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
confirmButton: (
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.confirmCreateButton'
|
||||
defaultMessage='Yes, Create'
|
||||
/>
|
||||
),
|
||||
confirmComplete: () => {
|
||||
this.handleCreateToken();
|
||||
trackEvent('settings', 'system_admin_create_user_access_token');
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleCreateToken();
|
||||
}
|
||||
|
||||
saveTokenKeyPress = (e) => {
|
||||
if (e.which === Constants.KeyCodes.ENTER) {
|
||||
this.confirmCreateToken();
|
||||
}
|
||||
}
|
||||
|
||||
confirmRevokeToken = (tokenId) => {
|
||||
const token = this.props.userAccessTokens[tokenId];
|
||||
|
||||
this.setState({
|
||||
showConfirmModal: true,
|
||||
confirmTitle: (
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.confirmDeleteTitle'
|
||||
defaultMessage='Delete {name} Token?'
|
||||
values={{
|
||||
name: token.description
|
||||
}}
|
||||
/>
|
||||
),
|
||||
confirmMessage: (
|
||||
<div className='alert alert-danger'>
|
||||
<FormattedHTMLMessage
|
||||
id='user.settings.tokens.confirmDeleteMessage'
|
||||
defaultMessage='Any integrations using this token will no longer be able to access the Mattermost API. You cannot undo this action. Are you sure want to delete this token?'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
confirmButton: (
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.confirmDeleteButton'
|
||||
defaultMessage='Yes, Delete'
|
||||
/>
|
||||
),
|
||||
confirmComplete: () => {
|
||||
this.revokeToken(tokenId);
|
||||
trackEvent('settings', 'revoke_user_access_token');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
revokeToken = async (tokenId) => {
|
||||
const {error} = await this.props.actions.revokeUserAccessToken(tokenId);
|
||||
if (error) {
|
||||
this.setState({serverError: error.message});
|
||||
}
|
||||
this.handleCancelConfirm();
|
||||
}
|
||||
|
||||
createTokensSection = () => {
|
||||
let updateSectionStatus;
|
||||
|
||||
if (this.props.activeSection === 'tokens') {
|
||||
const tokenList = [];
|
||||
Object.values(this.props.userAccessTokens).forEach((token) => {
|
||||
if (this.state.newToken && this.state.newToken.id === token.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
tokenList.push(
|
||||
<div
|
||||
key={token.id}
|
||||
className='setting-box__item'
|
||||
>
|
||||
<div className='whitespace--nowrap overflow--ellipsis'>
|
||||
<strong>{token.description}</strong>
|
||||
</div>
|
||||
<div className='setting-box__token-id whitespace--nowrap overflow--ellipsis'>
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.tokenId'
|
||||
defaultMessage='Token ID: '
|
||||
/>
|
||||
{token.id}
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
name={token.id}
|
||||
href='#'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.confirmRevokeToken(token.id);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.delete'
|
||||
defaultMessage='Delete'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<hr className='margin-bottom margin-top x2'/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
if (tokenList.length === 0) {
|
||||
tokenList.push(
|
||||
<FormattedMessage
|
||||
key='notokens'
|
||||
id='user.settings.tokens.userAccessTokensNone'
|
||||
defaultMessage='No user access tokens.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
let extraInfo;
|
||||
|
||||
if (isMobile()) {
|
||||
extraInfo = (
|
||||
<span>
|
||||
<FormattedHTMLMessage
|
||||
id='user.settings.tokens.description_mobile'
|
||||
defaultMessage='<a href="https://about.mattermost.com/default-user-access-tokens" target="_blank">User access tokens</a> function similar to session tokens and can be used by integrations to <a href="https://about.mattermost.com/default-api-authentication" target="_blank">authenticate against the REST API</a>. Create new tokens on your desktop.'
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
extraInfo = (
|
||||
<span>
|
||||
<FormattedHTMLMessage
|
||||
id='user.settings.tokens.description'
|
||||
defaultMessage='<a href="https://about.mattermost.com/default-user-access-tokens" target="_blank">User access tokens</a> function similar to session tokens and can be used by integrations to <a href="https://about.mattermost.com/default-api-authentication" target="_blank">authenticate against the REST API</a>.'
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let newTokenSection;
|
||||
if (this.state.tokenCreationState === TOKEN_CREATING) {
|
||||
newTokenSection = (
|
||||
<div className='padding-left x2'>
|
||||
<div className='row'>
|
||||
<label className='col-sm-auto control-label padding-right x2'>
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.name'
|
||||
defaultMessage='Name: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-5'>
|
||||
<input
|
||||
ref='newtokendescription'
|
||||
className='form-control'
|
||||
type='text'
|
||||
maxLength={64}
|
||||
onKeyPress={this.saveTokenKeyPress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='padding-top x2'>
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.nameDescription'
|
||||
defaultMessage='Give a name for your token, so you remember what it’s used for. A token is generated after you hit "Save".'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
id='clientError'
|
||||
className='has-error margin-top margin-bottom'
|
||||
>
|
||||
{this.state.tokenError}
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
className='btn btn-primary'
|
||||
onClick={this.confirmCreateToken}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-default'
|
||||
onClick={this.stopCreatingToken}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.tokenCreationState === TOKEN_CREATED) {
|
||||
newTokenSection = (
|
||||
<div
|
||||
className='alert alert-warning'
|
||||
>
|
||||
<i className='fa fa-warning margin-right'/>
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.copy'
|
||||
defaultMessage="Please copy the token below. You won't be able to see it again!"
|
||||
/>
|
||||
<br/>
|
||||
<br/>
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.name'
|
||||
defaultMessage='Name: '
|
||||
/>
|
||||
{this.state.newToken.description}
|
||||
<br/>
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.id'
|
||||
defaultMessage='ID: '
|
||||
/>
|
||||
{this.state.newToken.id}
|
||||
<br/>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.token'
|
||||
defaultMessage='Token: '
|
||||
/>
|
||||
{this.state.newToken.token}
|
||||
</strong>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
newTokenSection = (
|
||||
<a
|
||||
className='btn btn-primary'
|
||||
href='#'
|
||||
onClick={this.startCreatingToken}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='user.settings.tokens.create'
|
||||
defaultMessage='Create New Token'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const inputs = [];
|
||||
inputs.push(
|
||||
<div
|
||||
key='tokensSetting'
|
||||
className='padding-top'
|
||||
>
|
||||
<div key='tokenList'>
|
||||
<div className='alert alert-transparent'>
|
||||
{tokenList}
|
||||
</div>
|
||||
<br/>
|
||||
{newTokenSection}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
updateSectionStatus = function resetSection(e) {
|
||||
this.props.updateSection('');
|
||||
this.setState({newToken: null, tokenCreationState: TOKEN_NOT_CREATING, serverError: null, tokenError: ''});
|
||||
e.preventDefault();
|
||||
}.bind(this);
|
||||
|
||||
return (
|
||||
<SettingItemMax
|
||||
title={Utils.localizeMessage('user.settings.tokens.title', 'User Access Tokens')}
|
||||
inputs={inputs}
|
||||
extraInfo={extraInfo}
|
||||
infoPosition='top'
|
||||
server_error={this.state.serverError}
|
||||
updateSection={updateSectionStatus}
|
||||
width='full'
|
||||
cancelButtonText={
|
||||
<FormattedMessage
|
||||
id='user.settings.security.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const describe = Utils.localizeMessage('user.settings.tokens.clickToEdit', "Click 'Edit' to manage your user access tokens");
|
||||
|
||||
updateSectionStatus = function updateSection() {
|
||||
this.props.updateSection('tokens');
|
||||
}.bind(this);
|
||||
|
||||
return (
|
||||
<SettingItemMin
|
||||
title={Utils.localizeMessage('user.settings.tokens.title', 'User Access Tokens')}
|
||||
describe={describe}
|
||||
updateSection={updateSectionStatus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const user = this.props.user;
|
||||
const config = window.mm_config;
|
||||
@@ -959,6 +1358,11 @@ export default class SecurityTab extends React.Component {
|
||||
oauthSection = this.createOAuthAppsSection();
|
||||
}
|
||||
|
||||
let tokensSection;
|
||||
if (this.props.canUseAccessTokens) {
|
||||
tokensSection = this.createTokensSection();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='modal-header'>
|
||||
@@ -1001,6 +1405,8 @@ export default class SecurityTab extends React.Component {
|
||||
<div className='divider-light'/>
|
||||
{oauthSection}
|
||||
<div className='divider-light'/>
|
||||
{tokensSection}
|
||||
<div className='divider-light'/>
|
||||
{signInSection}
|
||||
<div className='divider-dark'/>
|
||||
<br/>
|
||||
@@ -1014,7 +1420,7 @@ export default class SecurityTab extends React.Component {
|
||||
defaultMessage='View Access History'
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
<b/>
|
||||
<br/>
|
||||
<ToggleModalButton
|
||||
className='security-links theme'
|
||||
dialogType={ActivityLogModal}
|
||||
@@ -1026,6 +1432,14 @@ export default class SecurityTab extends React.Component {
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
title={this.state.confirmTitle}
|
||||
message={this.state.confirmMessage}
|
||||
confirmButtonText={this.state.confirmButton}
|
||||
show={this.state.showConfirmModal}
|
||||
onConfirm={this.state.confirmComplete || (() => {})} //eslint-disable-line no-empty-function
|
||||
onCancel={this.handleCancelConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user