[MM-61716][MM-62643] Introduce Modal component, remove bootstrap and react-bootstrap from most modals, update styles (#3317)

* Introduce Modal, add some styles from the webapp

* Remove bootstrap from LoadingScreen/WelcomeScreen

* Migrate add/edit server modal to Modal, remove bootstrap

* Migrate remove server modal to Modal, remove bootstrap

* Migrate certificate modal to Modal, remove bootstrap

* Migrate login and permission modals to Modal, remove Bootstrap

* Misc fixes

* E2E tests for current modals

* Update src/renderer/components/Modal.tsx

Co-authored-by: Daniel Espino García <larkox@gmail.com>

* Update src/renderer/components/Modal.tsx

Co-authored-by: Daniel Espino García <larkox@gmail.com>

* PR feedback

---------

Co-authored-by: Daniel Espino García <larkox@gmail.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Devin Binnie
2025-02-18 10:58:28 -05:00
committed by GitHub
parent 34963cbaa8
commit 4d754efdd7
44 changed files with 1854 additions and 927 deletions

View File

@@ -3,20 +3,23 @@
// See LICENSE.txt for license information.
import React from 'react';
import {Button, Modal} from 'react-bootstrap';
import {Modal} from './Modal';
type Props = {
id: string;
title: string;
body: React.ReactNode;
acceptLabel: string;
cancelLabel: string;
onHide: () => void;
onAccept: React.MouseEventHandler<HTMLButtonElement>;
onCancel: React.MouseEventHandler<HTMLButtonElement>;
onAccept: () => void;
onCancel: () => void;
};
export default function DestructiveConfirmationModal(props: Props) {
const {
id,
title,
body,
acceptLabel,
@@ -27,23 +30,18 @@ export default function DestructiveConfirmationModal(props: Props) {
...rest} = props;
return (
<Modal
onHide={onHide}
id={id}
onExited={onHide}
isDeleteModal={true}
modalHeaderText={title}
handleCancel={onCancel}
handleConfirm={onAccept}
confirmButtonText={acceptLabel}
cancelButtonText={cancelLabel}
confirmButtonClassName='btn-danger'
{...rest}
>
<Modal.Header closeButton={true}>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
{body}
<Modal.Footer>
<Button
variant='link'
onClick={onCancel}
>{cancelLabel}</Button>
<Button
variant='danger'
onClick={onAccept}
>{acceptLabel}</Button>
</Modal.Footer>
</Modal>
);
}

View File

@@ -0,0 +1,274 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React, {useState, useRef, useEffect, useCallback} from 'react';
import {FormattedMessage} from 'react-intl';
import 'renderer/css/components/Modal.scss';
export type Props = {
id: string;
children: React.ReactNode;
onExited: () => void;
className?: string;
modalHeaderText?: React.ReactNode;
modalSubheaderText?: React.ReactNode;
show?: boolean;
handleCancel?: () => void;
handleConfirm?: () => void;
handleEnterKeyPress?: () => void;
handleKeydown?: (event?: React.KeyboardEvent<HTMLDivElement>) => void;
confirmButtonText?: React.ReactNode;
confirmButtonClassName?: string;
cancelButtonText?: React.ReactNode;
cancelButtonClassName?: string;
isConfirmDisabled?: boolean;
isDeleteModal?: boolean;
autoCloseOnCancelButton?: boolean;
autoCloseOnConfirmButton?: boolean;
ariaLabel?: string;
errorText?: string | React.ReactNode;
tabIndex?: number;
autoFocusConfirmButton?: boolean;
headerInput?: React.ReactNode;
bodyPadding?: boolean;
bodyDivider?: boolean;
footerContent?: React.ReactNode;
footerDivider?: boolean;
appendedContent?: React.ReactNode;
headerButton?: React.ReactNode;
};
export const Modal: React.FC<Props> = ({
id = 'modal',
children,
onExited,
className,
modalHeaderText,
modalSubheaderText,
show = true,
handleCancel,
handleConfirm,
handleEnterKeyPress,
handleKeydown,
confirmButtonText,
confirmButtonClassName,
cancelButtonText,
cancelButtonClassName,
isConfirmDisabled,
isDeleteModal,
autoCloseOnCancelButton = true,
autoCloseOnConfirmButton = true,
ariaLabel,
errorText,
tabIndex,
autoFocusConfirmButton,
headerInput,
bodyPadding = true,
bodyDivider,
footerContent,
footerDivider,
appendedContent,
headerButton,
}) => {
const [showState, setShowState] = useState<boolean>();
const backdropRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setShowState(show ?? true);
}, [show]);
const onHide = () => {
return new Promise<void>((resolve) => {
backdropRef.current?.addEventListener('transitionend', () => {
resolve();
}, {once: true});
setShowState(false);
});
};
const onClose = useCallback(async () => {
await onHide();
onExited();
}, [onExited]);
const handleCancelClick = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
if (autoCloseOnCancelButton) {
await onHide();
}
handleCancel?.();
};
const handleConfirmClick = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
if (autoCloseOnConfirmButton) {
await onHide();
}
handleConfirm?.();
};
const onEnterKeyDown = async (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter') {
if (event.nativeEvent.isComposing) {
return;
}
if (autoCloseOnConfirmButton) {
await onHide();
}
if (handleEnterKeyPress) {
handleEnterKeyPress();
}
}
handleKeydown?.(event);
};
let confirmButton;
if (handleConfirm) {
const isConfirmOrDeleteClassName = isDeleteModal ? 'delete' : 'confirm';
let confirmButtonTextNode: React.ReactNode = (
<FormattedMessage
id='modal.confirm'
defaultMessage='Confirm'
/>
);
if (confirmButtonText) {
confirmButtonTextNode = confirmButtonText;
}
confirmButton = (
<button
id={`${id}_confirm`}
autoFocus={autoFocusConfirmButton}
type='submit'
className={classNames('Modal__button btn btn-primary', isConfirmOrDeleteClassName, confirmButtonClassName, {
disabled: isConfirmDisabled,
})}
onClick={handleConfirmClick}
disabled={isConfirmDisabled}
>
{confirmButtonTextNode}
</button>
);
}
let cancelButton;
if (handleCancel) {
let cancelButtonTextNode: React.ReactNode = (
<FormattedMessage
id='modal.cancel'
defaultMessage='Cancel'
/>
);
if (cancelButtonText) {
cancelButtonTextNode = cancelButtonText;
}
cancelButton = (
<button
id={`${id}_cancel`}
type='button'
className={classNames('Modal__button btn btn-tertiary', cancelButtonClassName)}
onClick={handleCancelClick}
>
{cancelButtonTextNode}
</button>
);
}
const headerText = modalHeaderText && (
<div className='Modal__header'>
<h1
id='modalLabel'
className='Modal_title'
>
{modalHeaderText}
</h1>
{headerButton}
</div>
);
return (
<>
<div
ref={backdropRef}
className={classNames('Modal_backdrop fade', {show: showState})}
/>
<div
role='dialog'
className={classNames('Modal fade', {show: showState})}
onClick={onClose}
>
<div
id={id}
role='dialog'
aria-label={ariaLabel}
aria-labelledby={ariaLabel ? undefined : 'modalLabel'}
className={classNames(
'Modal_dialog Modal__compassDesign',
className,
)}
onClick={useCallback((event) => event.stopPropagation(), [])}
>
<div
onKeyDown={onEnterKeyDown}
tabIndex={tabIndex || 0}
className='Modal_content'
>
<div className='Modal_header'>
<div className='Modal__header__text_container'>
{headerText}
{headerInput}
{
modalSubheaderText &&
<div className='Modal_subheading-container'>
<p
id='Modal_subHeading'
className='Modal_subheading'
>
{modalSubheaderText}
</p>
</div>
}
</div>
<button
type='button'
className='close'
onClick={onClose}
>
<span aria-hidden='true'>{'×'}</span>
<span className='sr-only'>{'Close'}</span>
</button>
</div>
<div className={classNames('Modal_body', {divider: bodyDivider})}>
{errorText && (
<div className='Modal_error'>
<i className='icon icon-alert-outline'/>
<span>{errorText}</span>
</div>
)}
<div className={classNames('Modal__body', {padding: bodyPadding})}>
{children}
</div>
</div>
{(cancelButton || confirmButton || footerContent) && (
<div className={classNames('Modal_footer', {divider: footerDivider})}>
{(cancelButton || confirmButton) ? (
<>
{cancelButton}
{confirmButton}
</>
) : (
footerContent
)}
</div>
)}
{Boolean(appendedContent) && appendedContent}
</div>
</div>
</div>
</>
);
};

View File

@@ -3,7 +3,6 @@
// See LICENSE.txt for license information.
import React from 'react';
import {Modal, Button, FormGroup, FormControl, FormLabel, FormText, Spinner} from 'react-bootstrap';
import type {IntlShape} from 'react-intl';
import {FormattedMessage, injectIntl} from 'react-intl';
@@ -14,20 +13,22 @@ import type {UniqueServer} from 'types/config';
import type {Permissions} from 'types/permissions';
import type {URLValidationResult} from 'types/server';
import Input, {SIZE, STATUS} from './Input';
import {Modal} from './Modal';
import 'renderer/css/components/NewServerModal.scss';
type Props = {
onClose?: () => void;
onClose: () => void;
onSave?: (server: UniqueServer, permissions?: Permissions) => void;
server?: UniqueServer;
permissions?: Permissions;
editMode?: boolean;
show?: boolean;
restoreFocus?: boolean;
currentOrder?: number;
setInputRef?: (inputRef: HTMLInputElement) => void;
intl: IntlShape;
prefillURL?: string;
unremoveable?: boolean;
};
type State = {
@@ -49,10 +50,6 @@ class NewServerModal extends React.PureComponent<Props, State> {
validationTimeout?: NodeJS.Timeout;
mounted: boolean;
static defaultProps = {
restoreFocus: true,
};
constructor(props: Props) {
super(props);
@@ -159,19 +156,13 @@ class NewServerModal extends React.PureComponent<Props, State> {
getServerURLMessage = () => {
if (this.state.validationStarted) {
return (
<div>
<Spinner
className='NewServerModal-validationSpinner'
animation='border'
size='sm'
/>
<FormattedMessage
id='renderer.components.newServerModal.validating'
defaultMessage='Validating...'
/>
</div>
);
return {
type: STATUS.INFO,
value: this.props.intl.formatMessage({
id: 'renderer.components.newServerModal.validating',
defaultMessage: 'Validating...',
}),
};
}
if (!this.state.validationResult) {
@@ -180,114 +171,78 @@ class NewServerModal extends React.PureComponent<Props, State> {
switch (this.state.validationResult?.status) {
case URLValidationStatus.Missing:
return (
<div
id='urlValidation'
className='error'
>
<i className='icon-close-circle'/>
<FormattedMessage
id='renderer.components.newServerModal.error.urlRequired'
defaultMessage='URL is required.'
/>
</div>
);
return {
type: STATUS.ERROR,
value: this.props.intl.formatMessage({
id: 'renderer.components.newServerModal.error.urlRequired',
defaultMessage: 'URL is required.',
}),
};
case URLValidationStatus.Invalid:
return (
<div
id='urlValidation'
className='error'
>
<i className='icon-close-circle'/>
<FormattedMessage
id='renderer.components.newServerModal.error.urlIncorrectFormatting'
defaultMessage='URL is not formatted correctly.'
/>
</div>
);
return {
type: STATUS.ERROR,
value: this.props.intl.formatMessage({
id: 'renderer.components.newServerModal.error.urlIncorrectFormatting',
defaultMessage: 'URL is not formatted correctly.',
}),
};
case URLValidationStatus.URLExists:
return (
<div
id='urlValidation'
className='warning'
>
<i className='icon-alert-outline'/>
<FormattedMessage
id='renderer.components.newServerModal.error.serverUrlExists'
defaultMessage='A server named {serverName} with the same Site URL already exists.'
values={{serverName: this.state.validationResult.existingServerName}}
/>
</div>
);
return {
type: STATUS.WARNING,
value: this.props.intl.formatMessage({
id: 'renderer.components.newServerModal.error.serverUrlExists',
defaultMessage: 'A server named {serverName} with the same Site URL already exists.',
}, {
serverName: this.state.validationResult.existingServerName,
}),
};
case URLValidationStatus.Insecure:
return (
<div
id='urlValidation'
className='warning'
>
<i className='icon-alert-outline'/>
<FormattedMessage
id='renderer.components.newServerModal.warning.insecure'
defaultMessage='Your server URL is potentially insecure. For best results, use a URL with the HTTPS protocol.'
/>
</div>
);
return {
type: STATUS.WARNING,
value: this.props.intl.formatMessage({
id: 'renderer.components.newServerModal.warning.insecure',
defaultMessage: 'Your server URL is potentially insecure. For best results, use a URL with the HTTPS protocol.',
}),
};
case URLValidationStatus.NotMattermost:
return (
<div
id='urlValidation'
className='warning'
>
<i className='icon-alert-outline'/>
<FormattedMessage
id='renderer.components.newServerModal.warning.notMattermost'
defaultMessage='The server URL provided does not appear to point to a valid Mattermost server. Please verify the URL and check your connection.'
/>
</div>
);
return {
type: STATUS.WARNING,
value: this.props.intl.formatMessage({
id: 'renderer.components.newServerModal.warning.notMattermost',
defaultMessage: 'The server URL provided does not appear to point to a valid Mattermost server. Please verify the URL and check your connection.',
}),
};
case URLValidationStatus.URLNotMatched:
return (
<div
id='urlValidation'
className='warning'
>
<i className='icon-alert-outline'/>
<FormattedMessage
id='renderer.components.newServerModal.warning.urlNotMatched'
defaultMessage='The server URL does not match the configured Site URL on your Mattermost server. Server version: {serverVersion}'
values={{serverVersion: this.state.validationResult.serverVersion}}
/>
</div>
);
return {
type: STATUS.WARNING,
value: this.props.intl.formatMessage({
id: 'renderer.components.newServerModal.warning.urlNotMatched',
defaultMessage: 'The server URL does not match the configured Site URL on your Mattermost server. Server version: {serverVersion}',
}, {
serverVersion: this.state.validationResult.serverVersion,
}),
};
case URLValidationStatus.URLUpdated:
return (
<div
id='urlValidation'
className='info'
>
<i className='icon-information-outline'/>
<FormattedMessage
id='renderer.components.newServerModal.warning.urlUpdated'
defaultMessage='The server URL provided has been updated to match the configured Site URL on your Mattermost server. Server version: {serverVersion}'
values={{serverVersion: this.state.validationResult.serverVersion}}
/>
</div>
);
return {
type: STATUS.INFO,
value: this.props.intl.formatMessage({
id: 'renderer.components.newServerModal.warning.urlUpdated',
defaultMessage: 'The server URL provided has been updated to match the configured Site URL on your Mattermost server. Server version: {serverVersion}',
}, {
serverVersion: this.state.validationResult.serverVersion,
}),
};
}
return (
<div
id='urlValidation'
className='success'
>
<i className='icon-check-circle'/>
<FormattedMessage
id='renderer.components.newServerModal.success.ok'
defaultMessage='Server URL is valid. Server version: {serverVersion}'
values={{serverVersion: this.state.validationResult.serverVersion}}
/>
</div>
);
return {
type: STATUS.SUCCESS,
value: this.props.intl.formatMessage({
id: 'renderer.components.newServerModal.success.ok',
defaultMessage: 'Server URL is valid. Server version: {serverVersion}',
}, {
serverVersion: this.state.validationResult.serverVersion,
}),
};
};
openNotificationPrefs = () => {
@@ -303,19 +258,18 @@ class NewServerModal extends React.PureComponent<Props, State> {
};
getServerNameMessage = () => {
if (!this.state.validationResult) {
return null;
}
if (!this.state.serverName.length) {
return (
<div
id='nameValidation'
className='error'
>
<i className='icon-close-circle'/>
<FormattedMessage
id='renderer.components.newServerModal.error.nameRequired'
defaultMessage='Name is required.'
/>
</div>
);
return {
type: STATUS.ERROR,
value: this.props.intl.formatMessage({
id: 'renderer.components.newServerModal.error.nameRequired',
defaultMessage: 'Name is required.',
}),
};
}
return null;
};
@@ -401,114 +355,71 @@ class NewServerModal extends React.PureComponent<Props, State> {
return (
<Modal
bsClass='modal'
className='NewServerModal'
show={this.props.show}
id='newServerModal'
enforceFocus={true}
onEntered={() => this.serverUrlInputRef?.focus()}
onHide={this.props.onClose}
restoreFocus={this.props.restoreFocus}
onKeyDown={(e: React.KeyboardEvent) => {
switch (e.key) {
case 'Enter':
this.save();
// The add button from behind this might still be focused
e.preventDefault();
e.stopPropagation();
break;
case 'Escape':
this.props.onClose?.();
break;
}
}}
className='NewServerModal'
onExited={this.props.unremoveable ? () => {} : this.props.onClose}
modalHeaderText={this.getModalTitle()}
confirmButtonText={this.getSaveButtonLabel()}
handleConfirm={this.save}
isConfirmDisabled={!this.state.serverName.length || !this.state.validationResult || this.isServerURLErrored()}
handleCancel={this.props.onClose}
bodyDivider={true}
footerDivider={true}
>
<Modal.Header>
<Modal.Title>{this.getModalTitle()}</Modal.Title>
</Modal.Header>
<Modal.Body>
<>
{!(this.props.editMode && this.props.server?.isPredefined) &&
<>
<form>
<FormGroup>
<FormLabel>
<FormattedMessage
id='renderer.components.newServerModal.serverURL'
defaultMessage='Server URL'
/>
</FormLabel>
<FormControl
id='serverUrlInput'
type='text'
value={this.state.serverUrl}
placeholder='https://example.com'
onChange={this.handleServerUrlChange}
onClick={(e: React.MouseEvent<HTMLInputElement>) => {
e.stopPropagation();
}}
ref={(ref: HTMLInputElement) => {
this.serverUrlInputRef = ref;
if (this.props.setInputRef) {
this.props.setInputRef(ref);
}
}}
isInvalid={this.isServerURLErrored()}
autoFocus={true}
/>
<FormControl.Feedback/>
<FormText>
<FormattedMessage
id='renderer.components.newServerModal.serverURL.description'
defaultMessage='The URL of your Mattermost server. Must start with http:// or https://.'
/>
</FormText>
</FormGroup>
<FormGroup className='NewServerModal-noBottomSpace'>
<FormLabel>
<FormattedMessage
id='renderer.components.newServerModal.serverDisplayName'
defaultMessage='Server Display Name'
/>
</FormLabel>
<FormControl
id='serverNameInput'
type='text'
value={this.state.serverName}
placeholder={this.props.intl.formatMessage({id: 'renderer.components.newServerModal.serverDisplayName', defaultMessage: 'Server Display Name'})}
onChange={this.handleServerNameChange}
onClick={(e: React.MouseEvent<HTMLInputElement>) => {
e.stopPropagation();
}}
isInvalid={!this.state.serverName.length}
/>
<FormControl.Feedback/>
<FormText className='NewServerModal-noBottomSpace'>
<FormattedMessage
id='renderer.components.newServerModal.serverDisplayName.description'
defaultMessage='The name of the server displayed on your desktop app tab bar.'
/>
</FormText>
</FormGroup>
</form>
<div
className='NewServerModal-validation'
>
{this.getServerNameMessage()}
{this.getServerURLMessage()}
</div>
<Input
autoFocus={true}
id='serverUrlInput'
name='url'
type='text'
inputSize={SIZE.LARGE}
value={this.state.serverUrl}
onChange={this.handleServerUrlChange}
customMessage={this.getServerURLMessage() ?? ({
type: STATUS.INFO,
value: this.props.intl.formatMessage({
id: 'renderer.components.newServerModal.serverURL.description',
defaultMessage: 'The URL of your Mattermost server. Must start with http:// or https://.',
}),
})}
placeholder={this.props.intl.formatMessage({
id: 'renderer.components.newServerModal.serverURL',
defaultMessage: 'Server URL',
})}
/>
<Input
id='serverNameInput'
name='name'
type='text'
inputSize={SIZE.LARGE}
value={this.state.serverName}
onChange={this.handleServerNameChange}
customMessage={this.getServerNameMessage() ?? ({
type: STATUS.INFO,
value: this.props.intl.formatMessage({
id: 'renderer.components.newServerModal.serverDisplayName.description',
defaultMessage: 'The name of the server displayed on your desktop app tab bar.',
}),
})}
placeholder={this.props.intl.formatMessage({
id: 'renderer.components.newServerModal.serverDisplayName',
defaultMessage: 'Server display name',
})}
/>
</>
}
{this.props.editMode &&
<>
<hr/>
<h5>
<h3 className='NewServerModal__permissions__title'>
<FormattedMessage
id='renderer.components.newServerModal.permissions.title'
defaultMessage='Permissions'
/>
</h5>
</h3>
<Toggle
isChecked={this.state.permissions.media?.allowed}
onChange={this.handleChangePermission('media')}
@@ -520,7 +431,7 @@ class NewServerModal extends React.PureComponent<Props, State> {
defaultMessage='Microphone and Camera'
/>
{this.state.cameraDisabled &&
<FormText>
<small className='NewServerModal__toggle__description'>
<FormattedMessage
id='renderer.components.newServerModal.permissions.microphoneAndCamera.windowsCameraPermissions'
defaultMessage='Camera is disabled in Windows Settings. Click <link>here</link> to open the Camera Settings.'
@@ -535,10 +446,10 @@ class NewServerModal extends React.PureComponent<Props, State> {
),
}}
/>
</FormText>
</small>
}
{this.state.microphoneDisabled &&
<FormText>
<small className='NewServerModal__toggle__description'>
<FormattedMessage
id='renderer.components.newServerModal.permissions.microphoneAndCamera.windowsMicrophoneaPermissions'
defaultMessage='Microphone is disabled in Windows Settings. Click <link>here</link> to open the Microphone Settings.'
@@ -553,7 +464,7 @@ class NewServerModal extends React.PureComponent<Props, State> {
),
}}
/>
</FormText>
</small>
}
</div>
</Toggle>
@@ -568,22 +479,22 @@ class NewServerModal extends React.PureComponent<Props, State> {
defaultMessage='Notifications'
/>
{window.process.platform === 'darwin' &&
<FormText>
<FormattedMessage
id='renderer.components.newServerModal.permissions.notifications.mac'
defaultMessage='You may also need to enable notifications in macOS for Mattermost. Click <link>here</link> to open the System Preferences.'
values={notificationValues}
/>
</FormText>
<small className='NewServerModal__toggle__description'>
<FormattedMessage
id='renderer.components.newServerModal.permissions.notifications.mac'
defaultMessage='You may also need to enable notifications in macOS for Mattermost. Click <link>here</link> to open the System Preferences.'
values={notificationValues}
/>
</small>
}
{window.process.platform === 'win32' &&
<FormText>
<small className='NewServerModal__toggle__description'>
<FormattedMessage
id='renderer.components.newServerModal.permissions.notifications.windows'
defaultMessage='You may also need to enable notifications in Windows for Mattermost. Click <link>here</link> to open the Notification Settings.'
values={notificationValues}
/>
</FormText>
</small>
}
</div>
</Toggle>
@@ -609,33 +520,7 @@ class NewServerModal extends React.PureComponent<Props, State> {
</Toggle>
</>
}
</Modal.Body>
<Modal.Footer>
{this.props.onClose &&
<Button
id='cancelNewServerModal'
onClick={this.props.onClose}
variant='link'
>
<FormattedMessage
id='label.cancel'
defaultMessage='Cancel'
/>
</Button>
}
{this.props.onSave &&
<Button
id='saveNewServerModal'
onClick={this.save}
disabled={!this.state.serverName.length || !this.state.validationResult || this.isServerURLErrored()}
variant='primary'
>
{this.getSaveButtonLabel()}
</Button>
}
</Modal.Footer>
</>
</Modal>
);
}

View File

@@ -10,36 +10,32 @@ import DestructiveConfirmationModal from './DestructiveConfirmModal';
type Props = {
show: boolean;
serverName: string;
onHide: () => void;
onAccept: React.MouseEventHandler<HTMLButtonElement>;
onCancel: React.MouseEventHandler<HTMLButtonElement>;
onAccept: () => void;
onCancel: () => void;
};
function RemoveServerModal(props: Props) {
const intl = useIntl();
const {serverName, ...rest} = props;
const {...rest} = props;
return (
<DestructiveConfirmationModal
{...rest}
id='removeServerModal'
title={intl.formatMessage({id: 'renderer.components.removeServerModal.title', defaultMessage: 'Remove Server'})}
acceptLabel={intl.formatMessage({id: 'label.remove', defaultMessage: 'Remove'})}
cancelLabel={intl.formatMessage({id: 'label.cancel', defaultMessage: 'Cancel'})}
body={(
<Modal.Body>
<p>
<FormattedMessage
id='renderer.components.removeServerModal.body'
defaultMessage='This will remove the server from your Desktop App but will not delete any of its data - you can add the server back to the app at any time.'
/>
</p>
<p>
<FormattedMessage
id='renderer.components.removeServerModal.confirm'
defaultMessage='Confirm you wish to remove the {serverName} server?'
values={{serverName}}
/>
</p>
<FormattedMessage
id='renderer.components.removeServerModal.body'
defaultMessage='This will remove the server from your Desktop App but will not delete any of its data - you can add the server back at any time.'
/>
<br/><br/>
<FormattedMessage
id='renderer.components.removeServerModal.confirm'
defaultMessage='Are you sure you wish to remove the server?'
/>
</Modal.Body>
)}
/>

View File

@@ -3,9 +3,11 @@
import type {Certificate} from 'electron/renderer';
import React, {Fragment} from 'react';
import {Modal, Button, Row, Col} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {Modal} from 'renderer/components/Modal';
import IntlProvider from 'renderer/intl_provider';
type Props = {
certificate: Certificate;
onOk: () => void;
@@ -32,7 +34,7 @@ export default class ShowCertificateModal extends React.PureComponent<Props, Sta
const certificateSection = (descriptor: React.ReactNode) => {
return (
<Fragment>
<dt className={'certificate-key'}>{descriptor}</dt>
<dt className={'certificate-key'}><strong>{descriptor}</strong></dt>
<dd className={'certificate-section'}><span/></dd>
</Fragment>
);
@@ -41,29 +43,12 @@ export default class ShowCertificateModal extends React.PureComponent<Props, Sta
const val = value ? `${value}` : <span/>;
return (
<Fragment>
<dt className={'certificate-key'}>{descriptor}</dt>
<dt className={'certificate-key'}><strong>{descriptor}</strong></dt>
<dd className={'certificate-value'}>{val}</dd>
</Fragment>
);
};
if (this.state.certificate === null) {
return (
<Modal
bsClass='modal'
className='show-certificate'
onHide={() => {}}
>
<Modal.Body>
<FormattedMessage
id='renderer.components.showCertificateModal.noCertSelected'
defaultMessage='No certificate Selected'
/>
</Modal.Body>
</Modal>
);
}
const utcSeconds = (date: number) => {
const d = new Date(0);
d.setUTCSeconds(date);
@@ -74,18 +59,27 @@ export default class ShowCertificateModal extends React.PureComponent<Props, Sta
const creation = utcSeconds(this.state.certificate?.validStart || 0);
const dateDisplayOptions = {dateStyle: 'full' as const, timeStyle: 'full' as const};
const dateLocale = 'en-US'; // TODO: Translate?
return (
<Modal
bsClass='modal'
className='show-certificate'
show={this.state.certificate !== null}
onHide={() => {}}
>
<Modal.Header className={'no-border'}>
<Modal.Title>{'Certificate information'}</Modal.Title>
</Modal.Header>
<Modal.Body>
<p className='details'>{'Details'}</p>
<IntlProvider>
<Modal
id='showCertificateModal'
show={this.state.certificate !== null}
onExited={this.handleOk}
modalHeaderText={
<FormattedMessage
id='renderer.components.showCertificateModal.title'
defaultMessage='Certificate information'
/>
}
confirmButtonText={
<FormattedMessage
id='label.close'
defaultMessage='Close'
/>
}
handleConfirm={this.handleOk}
>
<dl>
{certificateSection(
<FormattedMessage
@@ -154,26 +148,8 @@ export default class ShowCertificateModal extends React.PureComponent<Props, Sta
this.state.certificate?.fingerprint.split('/')[0],
)}
</dl>
</Modal.Body>
<Modal.Footer className={'no-border'}>
<div className='container-fluid'>
<Row>
<Col>
<Button
variant='primary'
onClick={this.handleOk}
className={'primary'}
>
<FormattedMessage
id='label.close'
defaultMessage='Close'
/>
</Button>
</Col>
</Row>
</div>
</Modal.Footer>
</Modal>
</Modal>
</IntlProvider>
);
}
}