Migrate app to TypeScript (#1637)
* Initial setup and migrated src/common * WIP * WIP * WIP * Main module basically finished * Renderer process migrated * Added CI step and some fixes * Fixed remainder of issues and added proper ESLint config * Fixed a couple issues * Progress! * Some more fixes * Fixed a test * Fix build step * PR feedback
This commit is contained in:
@@ -1,35 +1,42 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Alert} from 'react-bootstrap';
|
||||
|
||||
const baseClassName = 'AutoSaveIndicator';
|
||||
const leaveClassName = `${baseClassName}-Leave`;
|
||||
|
||||
const SAVING_STATE_SAVING = 'saving';
|
||||
const SAVING_STATE_SAVED = 'saved';
|
||||
const SAVING_STATE_ERROR = 'error';
|
||||
const SAVING_STATE_DONE = 'done';
|
||||
export enum SavingState {
|
||||
SAVING_STATE_SAVING = 'saving',
|
||||
SAVING_STATE_SAVED = 'saved',
|
||||
SAVING_STATE_ERROR = 'error',
|
||||
SAVING_STATE_DONE = 'done',
|
||||
}
|
||||
|
||||
function getClassNameAndMessage(savingState, errorMessage) {
|
||||
function getClassNameAndMessage(savingState: SavingState, errorMessage?: string) {
|
||||
switch (savingState) {
|
||||
case SAVING_STATE_SAVING:
|
||||
case SavingState.SAVING_STATE_SAVING:
|
||||
return {className: baseClassName, message: 'Saving...'};
|
||||
case SAVING_STATE_SAVED:
|
||||
case SavingState.SAVING_STATE_SAVED:
|
||||
return {className: baseClassName, message: 'Saved'};
|
||||
case SAVING_STATE_ERROR:
|
||||
case SavingState.SAVING_STATE_ERROR:
|
||||
return {className: `${baseClassName}`, message: errorMessage};
|
||||
case SAVING_STATE_DONE:
|
||||
case SavingState.SAVING_STATE_DONE:
|
||||
return {className: `${baseClassName} ${leaveClassName}`, message: 'Saved'};
|
||||
default:
|
||||
return {className: `${baseClassName} ${leaveClassName}`, message: ''};
|
||||
}
|
||||
}
|
||||
|
||||
export default function AutoSaveIndicator(props) {
|
||||
type Props = {
|
||||
id?: string;
|
||||
savingState: SavingState;
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
export default function AutoSaveIndicator(props: Props) {
|
||||
const {savingState, errorMessage, ...rest} = props;
|
||||
const {className, message} = getClassNameAndMessage(savingState, errorMessage);
|
||||
return (
|
||||
@@ -42,15 +49,3 @@ export default function AutoSaveIndicator(props) {
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
AutoSaveIndicator.propTypes = {
|
||||
savingState: PropTypes.string.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
Object.assign(AutoSaveIndicator, {
|
||||
SAVING_STATE_SAVING,
|
||||
SAVING_STATE_SAVED,
|
||||
SAVING_STATE_ERROR,
|
||||
SAVING_STATE_DONE,
|
||||
});
|
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import {storiesOf} from '@storybook/react';
|
@@ -1,12 +1,21 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Modal} from 'react-bootstrap';
|
||||
|
||||
export default function DestructiveConfirmationModal(props) {
|
||||
type Props = {
|
||||
title: string;
|
||||
body: React.ReactNode;
|
||||
acceptLabel: string;
|
||||
cancelLabel: string;
|
||||
onHide: () => void;
|
||||
onAccept: React.MouseEventHandler<Button>;
|
||||
onCancel: React.MouseEventHandler<Button>;
|
||||
};
|
||||
|
||||
export default function DestructiveConfirmationModal(props: Props) {
|
||||
const {
|
||||
title,
|
||||
body,
|
||||
@@ -14,9 +23,13 @@ export default function DestructiveConfirmationModal(props) {
|
||||
cancelLabel,
|
||||
onAccept,
|
||||
onCancel,
|
||||
onHide,
|
||||
...rest} = props;
|
||||
return (
|
||||
<Modal {...rest}>
|
||||
<Modal
|
||||
onHide={onHide}
|
||||
{...rest}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>{title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
@@ -34,12 +47,3 @@ export default function DestructiveConfirmationModal(props) {
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
DestructiveConfirmationModal.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
body: PropTypes.node.isRequired,
|
||||
acceptLabel: PropTypes.string.isRequired,
|
||||
cancelLabel: PropTypes.string.isRequired,
|
||||
onAccept: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
@@ -1,14 +1,21 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
// ErrorCode: https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Grid, Row, Col} from 'react-bootstrap';
|
||||
|
||||
export default function ErrorView(props) {
|
||||
type Props = {
|
||||
errorInfo?: string;
|
||||
url?: string;
|
||||
id?: string;
|
||||
active?: boolean;
|
||||
appName?: string;
|
||||
};
|
||||
|
||||
export default function ErrorView(props: Props) {
|
||||
const classNames = ['container', 'ErrorView'];
|
||||
if (!props.active) {
|
||||
classNames.push('ErrorView-hidden');
|
||||
@@ -58,7 +65,7 @@ export default function ErrorView(props) {
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
{props.errorInfo.validatedURL}
|
||||
{props.url}
|
||||
</a>{' from a browser window.'}</li>
|
||||
</ul>
|
||||
<br/>
|
||||
@@ -78,11 +85,3 @@ export default function ErrorView(props) {
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorView.propTypes = {
|
||||
errorInfo: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
appName: PropTypes.string,
|
||||
};
|
@@ -2,10 +2,15 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Row, Button} from 'react-bootstrap';
|
||||
|
||||
export default class ExtraBar extends React.PureComponent {
|
||||
type Props = {
|
||||
darkMode?: boolean;
|
||||
goBack?: () => void;
|
||||
show?: boolean;
|
||||
};
|
||||
|
||||
export default class ExtraBar extends React.PureComponent<Props> {
|
||||
handleBack = () => {
|
||||
if (this.props.goBack) {
|
||||
this.props.goBack();
|
||||
@@ -42,9 +47,3 @@ export default class ExtraBar extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExtraBar.propTypes = {
|
||||
darkMode: PropTypes.bool,
|
||||
goBack: PropTypes.func,
|
||||
show: PropTypes.bool,
|
||||
};
|
@@ -3,11 +3,10 @@
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useAnimationEnd from '../../hooks/useAnimationEnd.js';
|
||||
import useAnimationEnd from '../../hooks/useAnimationEnd';
|
||||
|
||||
import LoadingIcon from './LoadingIcon.jsx';
|
||||
import LoadingIcon from './LoadingIcon';
|
||||
|
||||
const LOADING_STATE = {
|
||||
INITIALIZING: 'initializing', // animation graphics are hidden
|
||||
@@ -18,6 +17,12 @@ const LOADING_STATE = {
|
||||
|
||||
const ANIMATION_COMPLETION_DELAY = 500;
|
||||
|
||||
type Props = {
|
||||
loading: boolean;
|
||||
darkMode: boolean;
|
||||
onLoadAnimationComplete?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function component for rendering the animated MM logo loading sequence
|
||||
* @param {boolean} loading - Prop that indicates whether currently loading or not
|
||||
@@ -27,7 +32,7 @@ const ANIMATION_COMPLETION_DELAY = 500;
|
||||
function LoadingAnimation({
|
||||
loading = false,
|
||||
darkMode = false,
|
||||
onLoadAnimationComplete = null},
|
||||
onLoadAnimationComplete = undefined}: Props,
|
||||
) {
|
||||
const loadingIconContainerRef = React.useRef(null);
|
||||
const [animationState, setAnimationState] = React.useState(LOADING_STATE.INITIALIZING);
|
||||
@@ -55,14 +60,14 @@ function LoadingAnimation({
|
||||
}, [loadingAnimationComplete]);
|
||||
|
||||
// listen for end of the css logo animation sequence
|
||||
useAnimationEnd(loadingIconContainerRef, () => {
|
||||
useAnimationEnd<HTMLDivElement>(loadingIconContainerRef, () => {
|
||||
setTimeout(() => {
|
||||
setLoadingAnimationComplete(true);
|
||||
}, ANIMATION_COMPLETION_DELAY);
|
||||
}, 'LoadingAnimation__compass-shrink');
|
||||
|
||||
// listen for end of final css logo fade/shrink animation sequence
|
||||
useAnimationEnd(loadingIconContainerRef, () => {
|
||||
useAnimationEnd<HTMLDivElement>(loadingIconContainerRef, () => {
|
||||
if (onLoadAnimationComplete) {
|
||||
onLoadAnimationComplete();
|
||||
}
|
||||
@@ -84,10 +89,4 @@ function LoadingAnimation({
|
||||
);
|
||||
}
|
||||
|
||||
LoadingAnimation.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
darkMode: PropTypes.bool,
|
||||
onLoadAnimationComplete: PropTypes.func,
|
||||
};
|
||||
|
||||
export default LoadingAnimation;
|
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {default} from './LoadingAnimation.jsx';
|
||||
export {default} from './LoadingAnimation';
|
@@ -3,19 +3,24 @@
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useTransitionEnd from '../hooks/useTransitionEnd.js';
|
||||
import useTransitionEnd from '../hooks/useTransitionEnd';
|
||||
|
||||
import LoadingAnimation from './LoadingAnimation';
|
||||
|
||||
type Props = {
|
||||
loading?: boolean;
|
||||
darkMode?: boolean;
|
||||
onFadeOutComplete?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A function component for rendering the desktop app loading screen
|
||||
* @param {boolean} loading - Prop that indicates whether currently loading or not
|
||||
* @param {boolean} darkMode - Prop that indicates if dark mode is enabled
|
||||
* @param {() => void} onFadeOutComplete - Function to call when the loading animation is completely finished
|
||||
*/
|
||||
function LoadingScreen({loading = false, darkMode = false, onFadeOutComplete = () => null}) {
|
||||
function LoadingScreen({loading = false, darkMode = false, onFadeOutComplete = () => null}: Props) {
|
||||
const loadingScreenRef = React.useRef(null);
|
||||
|
||||
const [loadingIsComplete, setLoadingIsComplete] = React.useState(true);
|
||||
@@ -35,10 +40,10 @@ function LoadingScreen({loading = false, darkMode = false, onFadeOutComplete = (
|
||||
setLoadAnimationIsComplete(true);
|
||||
}
|
||||
|
||||
useTransitionEnd(loadingScreenRef, React.useCallback(() => {
|
||||
useTransitionEnd<HTMLDivElement>(loadingScreenRef, React.useCallback(() => {
|
||||
setFadeOutIsComplete(true);
|
||||
onFadeOutComplete();
|
||||
}), ['opacity']);
|
||||
}, []), ['opacity']);
|
||||
|
||||
function loadingInProgress() {
|
||||
return !(loadingIsComplete && loadAnimationIsComplete && fadeOutIsComplete);
|
||||
@@ -69,10 +74,4 @@ function LoadingScreen({loading = false, darkMode = false, onFadeOutComplete = (
|
||||
return loadingInProgress() ? loadingScreen : null;
|
||||
}
|
||||
|
||||
LoadingScreen.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
darkMode: PropTypes.bool,
|
||||
onFadeOutComplete: PropTypes.func,
|
||||
};
|
||||
|
||||
export default LoadingScreen;
|
@@ -1,11 +1,14 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React, {Fragment} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Grid, Row} from 'react-bootstrap';
|
||||
import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon';
|
||||
import {IpcRendererEvent} from 'electron/renderer';
|
||||
import {DropResult} from 'react-smooth-dnd';
|
||||
|
||||
import {Team} from 'types/config';
|
||||
|
||||
import {
|
||||
FOCUS_BROWSERVIEW,
|
||||
@@ -42,18 +45,54 @@ import closeButton from '../../assets/titlebar/chrome-close.svg';
|
||||
|
||||
import {playSound} from '../notificationSounds';
|
||||
|
||||
import TabBar from './TabBar.jsx';
|
||||
import ExtraBar from './ExtraBar.jsx';
|
||||
import ErrorView from './ErrorView.jsx';
|
||||
import TabBar from './TabBar';
|
||||
import ExtraBar from './ExtraBar';
|
||||
import ErrorView from './ErrorView';
|
||||
|
||||
const LOADING = 1;
|
||||
const DONE = 2;
|
||||
const RETRY = -1;
|
||||
const FAILED = 0;
|
||||
const NOSERVERS = -2;
|
||||
enum Status {
|
||||
LOADING = 1,
|
||||
DONE = 2,
|
||||
RETRY = -1,
|
||||
FAILED = 0,
|
||||
NOSERVERS = -2,
|
||||
}
|
||||
|
||||
export default class MainPage extends React.PureComponent {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
teams: Team[];
|
||||
showAddServerButton: boolean;
|
||||
moveTabs: (originalOrder: number, newOrder: number) => Promise<number | undefined>;
|
||||
openMenu: () => void;
|
||||
darkMode: boolean;
|
||||
appName: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
key: number;
|
||||
sessionsExpired: Record<string, boolean>;
|
||||
unreadCounts: Record<string, number>;
|
||||
mentionCounts: Record<string, number>;
|
||||
targetURL: string;
|
||||
maximized: boolean;
|
||||
tabStatus: Map<string, TabStatus>;
|
||||
darkMode: boolean;
|
||||
modalOpen?: boolean;
|
||||
fullScreen?: boolean;
|
||||
showExtraBar?: boolean;
|
||||
};
|
||||
|
||||
type TabStatus = {
|
||||
status: Status;
|
||||
extra?: {
|
||||
url: string;
|
||||
error: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default class MainPage extends React.PureComponent<Props, State> {
|
||||
topBar: React.RefObject<HTMLDivElement>;
|
||||
threeDotMenu: React.RefObject<HTMLButtonElement>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.topBar = React.createRef();
|
||||
@@ -66,7 +105,7 @@ export default class MainPage extends React.PureComponent {
|
||||
mentionCounts: {},
|
||||
targetURL: '',
|
||||
maximized: false,
|
||||
tabStatus: new Map(this.props.teams.map((server) => [server.name, {status: LOADING, extra: null}])),
|
||||
tabStatus: new Map(this.props.teams.map((server) => [server.name, {status: Status.LOADING}])),
|
||||
darkMode: this.props.darkMode,
|
||||
};
|
||||
}
|
||||
@@ -79,10 +118,10 @@ export default class MainPage extends React.PureComponent {
|
||||
return this.state.tabStatus.get(tabname);
|
||||
}
|
||||
}
|
||||
return {status: NOSERVERS};
|
||||
return {status: Status.NOSERVERS};
|
||||
}
|
||||
|
||||
updateTabStatus(server, newStatusValue) {
|
||||
updateTabStatus(server: string, newStatusValue: TabStatus) {
|
||||
const status = new Map(this.state.tabStatus);
|
||||
status.set(server, newStatusValue);
|
||||
this.setState({tabStatus: status});
|
||||
@@ -93,7 +132,7 @@ export default class MainPage extends React.PureComponent {
|
||||
window.ipcRenderer.on(LOAD_RETRY, (_, server, retry, err, loadUrl) => {
|
||||
console.log(`${server}: failed to load ${err}, but retrying`);
|
||||
const statusValue = {
|
||||
status: RETRY,
|
||||
status: Status.RETRY,
|
||||
extra: {
|
||||
retry,
|
||||
error: err,
|
||||
@@ -104,13 +143,13 @@ export default class MainPage extends React.PureComponent {
|
||||
});
|
||||
|
||||
window.ipcRenderer.on(LOAD_SUCCESS, (_, server) => {
|
||||
this.updateTabStatus(server, {status: DONE});
|
||||
this.updateTabStatus(server, {status: Status.DONE});
|
||||
});
|
||||
|
||||
window.ipcRenderer.on(LOAD_FAILED, (_, server, err, loadUrl) => {
|
||||
console.log(`${server}: failed to load ${err}`);
|
||||
const statusValue = {
|
||||
status: FAILED,
|
||||
status: Status.FAILED,
|
||||
extra: {
|
||||
error: err,
|
||||
url: loadUrl,
|
||||
@@ -198,44 +237,50 @@ export default class MainPage extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleMaximizeState = (_, maximized) => {
|
||||
handleMaximizeState = (_: IpcRendererEvent, maximized: boolean) => {
|
||||
this.setState({maximized});
|
||||
}
|
||||
|
||||
handleFullScreenState = (isFullScreen) => {
|
||||
handleFullScreenState = (isFullScreen: boolean) => {
|
||||
this.setState({fullScreen: isFullScreen});
|
||||
}
|
||||
|
||||
handleSetServerKey = (key) => {
|
||||
handleSetServerKey = (key: number) => {
|
||||
const newKey = (this.props.teams.length + key) % this.props.teams.length;
|
||||
this.setState({key: newKey});
|
||||
}
|
||||
|
||||
handleSelect = (name, key) => {
|
||||
handleSelect = (name: string, key: number) => {
|
||||
window.ipcRenderer.send(SWITCH_SERVER, name);
|
||||
this.handleSetServerKey(key);
|
||||
}
|
||||
|
||||
handleDragAndDrop = async (dropResult) => {
|
||||
handleDragAndDrop = async (dropResult: DropResult) => {
|
||||
const {removedIndex, addedIndex} = dropResult;
|
||||
if (removedIndex === null || addedIndex === null) {
|
||||
return;
|
||||
}
|
||||
if (removedIndex !== addedIndex) {
|
||||
const teamIndex = await this.props.moveTabs(removedIndex, addedIndex < this.props.teams.length ? addedIndex : this.props.teams.length - 1);
|
||||
if (!teamIndex) {
|
||||
return;
|
||||
}
|
||||
const name = this.props.teams[teamIndex].name;
|
||||
this.handleSelect(name, teamIndex);
|
||||
}
|
||||
}
|
||||
|
||||
handleClose = (e) => {
|
||||
handleClose = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation(); // since it is our button, the event goes into MainPage's onclick event, getting focus back.
|
||||
window.ipcRenderer.send(WINDOW_CLOSE);
|
||||
}
|
||||
|
||||
handleMinimize = (e) => {
|
||||
handleMinimize = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
window.ipcRenderer.send(WINDOW_MINIMIZE);
|
||||
}
|
||||
|
||||
handleMaximize = (e) => {
|
||||
handleMaximize = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
window.ipcRenderer.send(WINDOW_MAXIMIZE);
|
||||
}
|
||||
@@ -246,7 +291,7 @@ export default class MainPage extends React.PureComponent {
|
||||
|
||||
openMenu = () => {
|
||||
if (window.process.platform !== 'darwin') {
|
||||
this.threeDotMenu.current.blur();
|
||||
this.threeDotMenu.current?.blur();
|
||||
}
|
||||
this.props.openMenu();
|
||||
}
|
||||
@@ -263,10 +308,6 @@ export default class MainPage extends React.PureComponent {
|
||||
window.ipcRenderer.send(FOCUS_BROWSERVIEW);
|
||||
}
|
||||
|
||||
setInputRef = (ref) => {
|
||||
this.inputRef = ref;
|
||||
}
|
||||
|
||||
render() {
|
||||
const tabsRow = (
|
||||
<TabBar
|
||||
@@ -352,7 +393,7 @@ export default class MainPage extends React.PureComponent {
|
||||
>
|
||||
<div
|
||||
ref={this.topBar}
|
||||
className={`topBar-bg${this.state.unfocused ? ' unfocused' : ''}`}
|
||||
className={'topBar-bg'}
|
||||
>
|
||||
<button
|
||||
className='three-dot-menu'
|
||||
@@ -383,32 +424,29 @@ export default class MainPage extends React.PureComponent {
|
||||
return null;
|
||||
}
|
||||
switch (tabStatus.status) {
|
||||
case NOSERVERS: // TODO: substitute with https://mattermost.atlassian.net/browse/MM-25003
|
||||
case Status.NOSERVERS: // TODO: substitute with https://mattermost.atlassian.net/browse/MM-25003
|
||||
component = (
|
||||
<ErrorView
|
||||
id={'NoServers'}
|
||||
className='errorView'
|
||||
errorInfo={'No Servers configured'}
|
||||
url={tabStatus.extra ? tabStatus.extra.url : ''}
|
||||
active={true}
|
||||
retry={null}
|
||||
appName={this.props.appName}
|
||||
/>);
|
||||
break;
|
||||
case FAILED:
|
||||
case Status.FAILED:
|
||||
component = (
|
||||
<ErrorView
|
||||
id={this.state.key + '-fail'}
|
||||
className='errorView'
|
||||
errorInfo={tabStatus.extra ? tabStatus.extra.error : null}
|
||||
errorInfo={tabStatus.extra?.error}
|
||||
url={tabStatus.extra ? tabStatus.extra.url : ''}
|
||||
active={true}
|
||||
appName={this.props.appName}
|
||||
/>);
|
||||
break;
|
||||
case LOADING:
|
||||
case RETRY:
|
||||
case DONE:
|
||||
case Status.LOADING:
|
||||
case Status.RETRY:
|
||||
case Status.DONE:
|
||||
component = null;
|
||||
}
|
||||
return component;
|
||||
@@ -441,12 +479,3 @@ export default class MainPage extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MainPage.propTypes = {
|
||||
teams: PropTypes.array.isRequired,
|
||||
showAddServerButton: PropTypes.bool.isRequired,
|
||||
moveTabs: PropTypes.func.isRequired,
|
||||
openMenu: PropTypes.func.isRequired,
|
||||
darkMode: PropTypes.bool.isRequired,
|
||||
appName: PropTypes.string.isRequired,
|
||||
};
|
@@ -1,19 +1,42 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal, Button, FormGroup, FormControl, ControlLabel, HelpBlock} from 'react-bootstrap';
|
||||
|
||||
import {TeamWithIndex} from 'types/config';
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
export default class NewTeamModal extends React.PureComponent {
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
onSave?: (team: TeamWithIndex) => void;
|
||||
team?: TeamWithIndex;
|
||||
editMode?: boolean;
|
||||
show?: boolean;
|
||||
restoreFocus?: boolean;
|
||||
currentOrder?: number;
|
||||
setInputRef?: (inputRef: HTMLInputElement) => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
teamName: string;
|
||||
teamUrl: string;
|
||||
teamIndex?: number;
|
||||
teamOrder: number;
|
||||
saveStarted: boolean;
|
||||
}
|
||||
|
||||
export default class NewTeamModal extends React.PureComponent<Props, State> {
|
||||
wasShown?: boolean;
|
||||
teamNameInputRef?: HTMLInputElement;
|
||||
|
||||
static defaultProps = {
|
||||
restoreFocus: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.wasShown = false;
|
||||
@@ -29,7 +52,7 @@ export default class NewTeamModal extends React.PureComponent {
|
||||
this.setState({
|
||||
teamName: this.props.team ? this.props.team.name : '',
|
||||
teamUrl: this.props.team ? this.props.team.url : '',
|
||||
teamIndex: this.props.team ? this.props.team.index : false,
|
||||
teamIndex: this.props.team?.index,
|
||||
teamOrder: this.props.team ? this.props.team.order : (this.props.currentOrder || 0),
|
||||
saveStarted: false,
|
||||
});
|
||||
@@ -46,7 +69,7 @@ export default class NewTeamModal extends React.PureComponent {
|
||||
return this.getTeamNameValidationError() === null ? null : 'error';
|
||||
}
|
||||
|
||||
handleTeamNameChange = (e) => {
|
||||
handleTeamNameChange = (e: React.ChangeEvent<FormControl & HTMLInputElement>) => {
|
||||
this.setState({
|
||||
teamName: e.target.value,
|
||||
});
|
||||
@@ -72,7 +95,7 @@ export default class NewTeamModal extends React.PureComponent {
|
||||
return this.getTeamUrlValidationError() === null ? null : 'error';
|
||||
}
|
||||
|
||||
handleTeamUrlChange = (e) => {
|
||||
handleTeamUrlChange = (e: React.ChangeEvent<FormControl & HTMLInputElement>) => {
|
||||
this.setState({
|
||||
teamUrl: e.target.value,
|
||||
});
|
||||
@@ -102,10 +125,10 @@ export default class NewTeamModal extends React.PureComponent {
|
||||
saveStarted: true,
|
||||
}, () => {
|
||||
if (this.validateForm()) {
|
||||
this.props.onSave({
|
||||
this.props.onSave?.({
|
||||
url: this.state.teamUrl,
|
||||
name: this.state.teamName,
|
||||
index: this.state.teamIndex,
|
||||
index: this.state.teamIndex!,
|
||||
order: this.state.teamOrder,
|
||||
});
|
||||
}
|
||||
@@ -139,7 +162,7 @@ export default class NewTeamModal extends React.PureComponent {
|
||||
show={this.props.show}
|
||||
id='newServerModal'
|
||||
enforceFocus={true}
|
||||
onEntered={() => this.teamNameInputRef.focus()}
|
||||
onEntered={() => this.teamNameInputRef?.focus()}
|
||||
onHide={this.props.onClose}
|
||||
restoreFocus={this.props.restoreFocus}
|
||||
onKeyDown={(e) => {
|
||||
@@ -231,14 +254,3 @@ export default class NewTeamModal extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NewTeamModal.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
team: PropTypes.object,
|
||||
editMode: PropTypes.bool,
|
||||
show: PropTypes.bool,
|
||||
restoreFocus: PropTypes.bool,
|
||||
currentOrder: PropTypes.number,
|
||||
setInputRef: PropTypes.func,
|
||||
};
|
@@ -1,14 +1,21 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {Button, Modal} from 'react-bootstrap';
|
||||
|
||||
import DestructiveConfirmationModal from './DestructiveConfirmModal.jsx';
|
||||
import DestructiveConfirmationModal from './DestructiveConfirmModal';
|
||||
|
||||
export default function RemoveServerModal(props) {
|
||||
type Props = {
|
||||
show: boolean;
|
||||
serverName: string;
|
||||
onHide: () => void;
|
||||
onAccept: React.MouseEventHandler<Button>;
|
||||
onCancel: React.MouseEventHandler<Button>;
|
||||
}
|
||||
|
||||
export default function RemoveServerModal(props: Props) {
|
||||
const {serverName, ...rest} = props;
|
||||
return (
|
||||
<DestructiveConfirmationModal
|
||||
@@ -30,7 +37,3 @@ export default function RemoveServerModal(props) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
RemoveServerModal.propTypes = {
|
||||
serverName: PropTypes.string.isRequired,
|
||||
};
|
@@ -1,38 +1,79 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import 'renderer/css/settings.css';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Checkbox, Col, FormGroup, Grid, HelpBlock, Navbar, Radio, Row, Button} from 'react-bootstrap';
|
||||
|
||||
import {debounce} from 'underscore';
|
||||
|
||||
import {CombinedConfig, LocalConfiguration, Team} from 'types/config';
|
||||
import {DeepPartial} from 'types/utils';
|
||||
|
||||
import {GET_LOCAL_CONFIGURATION, UPDATE_CONFIGURATION, DOUBLE_CLICK_ON_WINDOW, GET_DOWNLOAD_LOCATION, SWITCH_SERVER, ADD_SERVER, RELOAD_CONFIGURATION} from 'common/communication';
|
||||
|
||||
import TeamList from './TeamList.jsx';
|
||||
import AutoSaveIndicator from './AutoSaveIndicator.jsx';
|
||||
import TeamList from './TeamList';
|
||||
import AutoSaveIndicator, {SavingState} from './AutoSaveIndicator';
|
||||
|
||||
const CONFIG_TYPE_SERVERS = 'servers';
|
||||
const CONFIG_TYPE_APP_OPTIONS = 'appOptions';
|
||||
|
||||
function backToIndex(serverName) {
|
||||
type ConfigType = typeof CONFIG_TYPE_SERVERS | typeof CONFIG_TYPE_APP_OPTIONS;
|
||||
|
||||
type State = DeepPartial<CombinedConfig> & {
|
||||
ready: boolean;
|
||||
maximized?: boolean;
|
||||
teams?: Team[];
|
||||
showAddTeamForm: boolean;
|
||||
trayWasVisible?: boolean;
|
||||
firstRun?: boolean;
|
||||
savingState: SavingStateItems;
|
||||
userOpenedDownloadDialog: boolean;
|
||||
}
|
||||
|
||||
type SavingStateItems = {
|
||||
appOptions: SavingState;
|
||||
servers: SavingState;
|
||||
};
|
||||
|
||||
type SaveQueueItem = {
|
||||
configType: ConfigType;
|
||||
key: keyof CombinedConfig;
|
||||
data: CombinedConfig[keyof CombinedConfig];
|
||||
}
|
||||
|
||||
function backToIndex(serverName: string) {
|
||||
window.ipcRenderer.send(SWITCH_SERVER, serverName);
|
||||
window.close();
|
||||
}
|
||||
|
||||
export default class SettingsPage extends React.PureComponent {
|
||||
constructor(props) {
|
||||
export default class SettingsPage extends React.PureComponent<Record<string, never>, State> {
|
||||
trayIconThemeRef: React.RefObject<FormGroup>;
|
||||
downloadLocationRef: React.RefObject<HTMLInputElement>;
|
||||
showTrayIconRef: React.RefObject<Checkbox>;
|
||||
autostartRef: React.RefObject<Checkbox>;
|
||||
minimizeToTrayRef: React.RefObject<Checkbox>;
|
||||
flashWindowRef: React.RefObject<Checkbox>;
|
||||
bounceIconRef: React.RefObject<Checkbox>;
|
||||
showUnreadBadgeRef: React.RefObject<Checkbox>;
|
||||
useSpellCheckerRef: React.RefObject<Checkbox>;
|
||||
enableHardwareAccelerationRef: React.RefObject<Checkbox>;
|
||||
|
||||
saveQueue: SaveQueueItem[];
|
||||
|
||||
constructor(props: Record<string, never>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
ready: false,
|
||||
teams: [],
|
||||
showAddTeamForm: false,
|
||||
savingState: {
|
||||
appOptions: AutoSaveIndicator.SAVING_STATE_DONE,
|
||||
servers: AutoSaveIndicator.SAVING_STATE_DONE,
|
||||
appOptions: SavingState.SAVING_STATE_DONE,
|
||||
servers: SavingState.SAVING_STATE_DONE,
|
||||
},
|
||||
userOpenedDownloadDialog: false,
|
||||
};
|
||||
@@ -67,26 +108,26 @@ export default class SettingsPage extends React.PureComponent {
|
||||
|
||||
getConfig = () => {
|
||||
window.ipcRenderer.invoke(GET_LOCAL_CONFIGURATION).then((config) => {
|
||||
this.setState({ready: true, maximized: false, ...this.convertConfigDataToState(config)});
|
||||
this.setState({ready: true, maximized: false, ...this.convertConfigDataToState(config) as Omit<State, 'ready'>});
|
||||
});
|
||||
}
|
||||
|
||||
convertConfigDataToState = (configData, currentState = {}) => {
|
||||
const newState = Object.assign({}, configData);
|
||||
convertConfigDataToState = (configData: Partial<LocalConfiguration>, currentState: Partial<State> = {}) => {
|
||||
const newState = Object.assign({} as State, configData);
|
||||
newState.showAddTeamForm = currentState.showAddTeamForm || false;
|
||||
newState.trayWasVisible = currentState.trayWasVisible || false;
|
||||
if (newState.teams.length === 0 && currentState.firstRun !== false) {
|
||||
if (newState.teams?.length === 0 && currentState.firstRun !== false) {
|
||||
newState.firstRun = false;
|
||||
newState.showAddTeamForm = true;
|
||||
}
|
||||
newState.savingState = currentState.savingState || {
|
||||
appOptions: AutoSaveIndicator.SAVING_STATE_DONE,
|
||||
servers: AutoSaveIndicator.SAVING_STATE_DONE,
|
||||
appOptions: SavingState.SAVING_STATE_DONE,
|
||||
servers: SavingState.SAVING_STATE_DONE,
|
||||
};
|
||||
return newState;
|
||||
}
|
||||
|
||||
saveSetting = (configType, {key, data}) => {
|
||||
saveSetting = (configType: ConfigType, {key, data}: {key: keyof CombinedConfig; data: CombinedConfig[keyof CombinedConfig]}) => {
|
||||
this.saveQueue.push({
|
||||
configType,
|
||||
key,
|
||||
@@ -115,25 +156,25 @@ export default class SettingsPage extends React.PureComponent {
|
||||
|
||||
Object.entries(queuedUpdateCounts).forEach(([configType, count]) => {
|
||||
if (count > 0) {
|
||||
savingState[configType] = AutoSaveIndicator.SAVING_STATE_SAVING;
|
||||
} else if (count === 0 && savingState[configType] === AutoSaveIndicator.SAVING_STATE_SAVING) {
|
||||
savingState[configType] = AutoSaveIndicator.SAVING_STATE_SAVED;
|
||||
this.resetSaveState(configType);
|
||||
savingState[configType as keyof SavingStateItems] = SavingState.SAVING_STATE_SAVING;
|
||||
} else if (count === 0 && savingState[configType as keyof SavingStateItems] === SavingState.SAVING_STATE_SAVING) {
|
||||
savingState[configType as keyof SavingStateItems] = SavingState.SAVING_STATE_SAVED;
|
||||
this.resetSaveState(configType as keyof SavingStateItems);
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({savingState});
|
||||
}
|
||||
|
||||
resetSaveState = debounce((configType) => {
|
||||
if (this.state.savingState[configType] !== AutoSaveIndicator.SAVING_STATE_SAVING) {
|
||||
resetSaveState = debounce((configType: keyof SavingStateItems) => {
|
||||
if (this.state.savingState[configType] !== SavingState.SAVING_STATE_SAVING) {
|
||||
const savingState = Object.assign({}, this.state.savingState);
|
||||
savingState[configType] = AutoSaveIndicator.SAVING_STATE_DONE;
|
||||
savingState[configType] = SavingState.SAVING_STATE_DONE;
|
||||
this.setState({savingState});
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
handleTeamsChange = (teams) => {
|
||||
handleTeamsChange = (teams: Team[]) => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
|
||||
this.setState({
|
||||
showAddTeamForm: false,
|
||||
@@ -145,7 +186,7 @@ export default class SettingsPage extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleChangeShowTrayIcon = () => {
|
||||
const shouldShowTrayIcon = !this.showTrayIconRef.current.props.checked;
|
||||
const shouldShowTrayIcon = !this.showTrayIconRef.current?.props.checked;
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showTrayIcon', data: shouldShowTrayIcon});
|
||||
this.setState({
|
||||
showTrayIcon: shouldShowTrayIcon,
|
||||
@@ -158,7 +199,7 @@ export default class SettingsPage extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeTrayIconTheme = (theme) => {
|
||||
handleChangeTrayIconTheme = (theme: string) => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'trayIconTheme', data: theme});
|
||||
this.setState({
|
||||
trayIconTheme: theme,
|
||||
@@ -166,14 +207,14 @@ export default class SettingsPage extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleChangeAutoStart = () => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'autostart', data: !this.autostartRef.current.props.checked});
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'autostart', data: !this.autostartRef.current?.props.checked});
|
||||
this.setState({
|
||||
autostart: !this.autostartRef.current.props.checked,
|
||||
autostart: !this.autostartRef.current?.props.checked,
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeMinimizeToTray = () => {
|
||||
const shouldMinimizeToTray = this.state.showTrayIcon && !this.minimizeToTrayRef.current.props.checked;
|
||||
const shouldMinimizeToTray = this.state.showTrayIcon && !this.minimizeToTrayRef.current?.props.checked;
|
||||
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'minimizeToTray', data: shouldMinimizeToTray});
|
||||
this.setState({
|
||||
@@ -185,10 +226,10 @@ export default class SettingsPage extends React.PureComponent {
|
||||
this.setState({
|
||||
showAddTeamForm: !this.state.showAddTeamForm,
|
||||
});
|
||||
document.activeElement.blur();
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
}
|
||||
|
||||
setShowTeamFormVisibility = (val) => {
|
||||
setShowTeamFormVisibility = (val: boolean) => {
|
||||
this.setState({
|
||||
showAddTeamForm: val,
|
||||
});
|
||||
@@ -199,13 +240,13 @@ export default class SettingsPage extends React.PureComponent {
|
||||
key: 'notifications',
|
||||
data: {
|
||||
...this.state.notifications,
|
||||
flashWindow: this.flashWindowRef.current.props.checked ? 0 : 2,
|
||||
flashWindow: this.flashWindowRef.current?.props.checked ? 0 : 2,
|
||||
},
|
||||
});
|
||||
this.setState({
|
||||
notifications: {
|
||||
...this.state.notifications,
|
||||
flashWindow: this.flashWindowRef.current.props.checked ? 0 : 2,
|
||||
flashWindow: this.flashWindowRef.current?.props.checked ? 0 : 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -215,18 +256,18 @@ export default class SettingsPage extends React.PureComponent {
|
||||
key: 'notifications',
|
||||
data: {
|
||||
...this.state.notifications,
|
||||
bounceIcon: !this.bounceIconRef.current.props.checked,
|
||||
bounceIcon: !this.bounceIconRef.current?.props.checked,
|
||||
},
|
||||
});
|
||||
this.setState({
|
||||
notifications: {
|
||||
...this.state.notifications,
|
||||
bounceIcon: !this.bounceIconRef.current.props.checked,
|
||||
bounceIcon: !this.bounceIconRef.current?.props.checked,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleBounceIconType = (event) => {
|
||||
handleBounceIconType = (event: React.ChangeEvent<Radio & HTMLInputElement>) => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {
|
||||
key: 'notifications',
|
||||
data: {
|
||||
@@ -237,33 +278,33 @@ export default class SettingsPage extends React.PureComponent {
|
||||
this.setState({
|
||||
notifications: {
|
||||
...this.state.notifications,
|
||||
bounceIconType: event.target.value,
|
||||
bounceIconType: event.target.value as 'critical' | 'informational',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleShowUnreadBadge = () => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showUnreadBadge', data: !this.showUnreadBadgeRef.current.props.checked});
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showUnreadBadge', data: !this.showUnreadBadgeRef.current?.props.checked});
|
||||
this.setState({
|
||||
showUnreadBadge: !this.showUnreadBadgeRef.current.props.checked,
|
||||
showUnreadBadge: !this.showUnreadBadgeRef.current?.props.checked,
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeUseSpellChecker = () => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'useSpellChecker', data: !this.useSpellCheckerRef.current.props.checked});
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'useSpellChecker', data: !this.useSpellCheckerRef.current?.props.checked});
|
||||
this.setState({
|
||||
useSpellChecker: !this.useSpellCheckerRef.current.props.checked,
|
||||
useSpellChecker: !this.useSpellCheckerRef.current?.props.checked,
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeEnableHardwareAcceleration = () => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'enableHardwareAcceleration', data: !this.enableHardwareAccelerationRef.current.props.checked});
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'enableHardwareAcceleration', data: !this.enableHardwareAccelerationRef.current?.props.checked});
|
||||
this.setState({
|
||||
enableHardwareAcceleration: !this.enableHardwareAccelerationRef.current.props.checked,
|
||||
enableHardwareAcceleration: !this.enableHardwareAccelerationRef.current?.props.checked,
|
||||
});
|
||||
}
|
||||
|
||||
saveDownloadLocation = (location) => {
|
||||
saveDownloadLocation = (location: string) => {
|
||||
if (!location) {
|
||||
return;
|
||||
}
|
||||
@@ -273,7 +314,7 @@ export default class SettingsPage extends React.PureComponent {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'downloadLocation', data: location});
|
||||
}
|
||||
|
||||
handleChangeDownloadLocation = (e) => {
|
||||
handleChangeDownloadLocation = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.saveDownloadLocation(e.target.value);
|
||||
}
|
||||
|
||||
@@ -285,8 +326,8 @@ export default class SettingsPage extends React.PureComponent {
|
||||
this.setState({userOpenedDownloadDialog: false});
|
||||
}
|
||||
|
||||
updateTeam = (index, newData) => {
|
||||
const teams = this.state.teams;
|
||||
updateTeam = (index: number, newData: Team) => {
|
||||
const teams = this.state.teams || [];
|
||||
teams[index] = newData;
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
|
||||
this.setState({
|
||||
@@ -294,8 +335,8 @@ export default class SettingsPage extends React.PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
addServer = (team) => {
|
||||
const teams = this.state.teams;
|
||||
addServer = (team: Team) => {
|
||||
const teams = this.state.teams || [];
|
||||
teams.push(team);
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
|
||||
this.setState({
|
||||
@@ -303,12 +344,6 @@ export default class SettingsPage extends React.PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
openMenu = () => {
|
||||
// @eslint-ignore
|
||||
this.threeDotMenu.current.blur();
|
||||
this.props.openMenu();
|
||||
}
|
||||
|
||||
handleDoubleClick = () => {
|
||||
window.ipcRenderer.send(DOUBLE_CLICK_ON_WINDOW, 'settings');
|
||||
}
|
||||
@@ -317,7 +352,7 @@ export default class SettingsPage extends React.PureComponent {
|
||||
const settingsPage = {
|
||||
navbar: {
|
||||
backgroundColor: '#fff',
|
||||
position: 'relative',
|
||||
position: 'relative' as const,
|
||||
},
|
||||
close: {
|
||||
textDecoration: 'none',
|
||||
@@ -329,7 +364,7 @@ export default class SettingsPage extends React.PureComponent {
|
||||
color: '#bbb',
|
||||
},
|
||||
heading: {
|
||||
textAlign: 'center',
|
||||
textAlign: 'center' as const,
|
||||
fontSize: '24px',
|
||||
margin: '0',
|
||||
padding: '1em 0',
|
||||
@@ -356,7 +391,7 @@ export default class SettingsPage extends React.PureComponent {
|
||||
padding: '0 12px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
fontWeight: '500',
|
||||
fontWeight: 500,
|
||||
},
|
||||
|
||||
downloadLocationButton: {
|
||||
@@ -372,14 +407,12 @@ export default class SettingsPage extends React.PureComponent {
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<TeamList
|
||||
teams={this.state.teams}
|
||||
teams={this.state.teams!}
|
||||
showAddTeamForm={this.state.showAddTeamForm}
|
||||
toggleAddTeamForm={this.toggleShowTeamForm}
|
||||
setAddTeamFormVisibility={this.setShowTeamFormVisibility}
|
||||
onTeamsChange={this.handleTeamsChange}
|
||||
updateTeam={this.updateTeam}
|
||||
addServer={this.addServer}
|
||||
allowTeamEdit={this.state.enableServerManagement}
|
||||
onTeamClick={(name) => {
|
||||
backToIndex(name);
|
||||
}}
|
||||
@@ -576,7 +609,7 @@ export default class SettingsPage extends React.PureComponent {
|
||||
name='trayIconTheme'
|
||||
value='light'
|
||||
defaultChecked={this.state.trayIconTheme === 'light' || !this.state.trayIconTheme}
|
||||
onChange={(event) => this.handleChangeTrayIconTheme('light', event)}
|
||||
onChange={() => this.handleChangeTrayIconTheme('light')}
|
||||
>
|
||||
{'Light'}
|
||||
</Radio>
|
||||
@@ -586,7 +619,7 @@ export default class SettingsPage extends React.PureComponent {
|
||||
name='trayIconTheme'
|
||||
value='dark'
|
||||
defaultChecked={this.state.trayIconTheme === 'dark'}
|
||||
onChange={(event) => this.handleChangeTrayIconTheme('dark', event)}
|
||||
onChange={() => this.handleChangeTrayIconTheme('dark')}
|
||||
>{'Dark'}</Radio>
|
||||
</FormGroup>,
|
||||
);
|
||||
@@ -719,7 +752,3 @@ export default class SettingsPage extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsPage.propTypes = {
|
||||
openMenu: PropTypes.func.isRequired,
|
||||
};
|
@@ -1,17 +1,39 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Nav, NavItem} from 'react-bootstrap';
|
||||
import {Container, Draggable} from 'react-smooth-dnd';
|
||||
import {Container, Draggable, OnDropCallback} from 'react-smooth-dnd';
|
||||
import PlusIcon from 'mdi-react/PlusIcon';
|
||||
|
||||
import {Team} from 'types/config';
|
||||
|
||||
import {GET_CONFIGURATION} from 'common/communication';
|
||||
|
||||
export default class TabBar extends React.PureComponent { // need "this"
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
activeKey: number;
|
||||
id: string;
|
||||
isDarkMode: boolean;
|
||||
onSelect: (name: string, index: number) => void;
|
||||
teams: Team[];
|
||||
sessionsExpired: Record<string, boolean>;
|
||||
unreadCounts: Record<string, number>;
|
||||
mentionCounts: Record<string, number>;
|
||||
showAddServerButton: boolean;
|
||||
onAddServer: () => void;
|
||||
onDrop: OnDropCallback;
|
||||
tabsDisabled?: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
hasGPOTeams: boolean;
|
||||
};
|
||||
|
||||
export default class TabBar extends React.PureComponent<Props, State> { // need "this"
|
||||
container?: React.RefObject<Container>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasGPOTeams: false,
|
||||
@@ -37,7 +59,7 @@ export default class TabBar extends React.PureComponent { // need "this"
|
||||
mentionCount = this.props.mentionCounts[index];
|
||||
}
|
||||
|
||||
let badgeDiv;
|
||||
let badgeDiv: React.ReactNode;
|
||||
if (sessionExpired) {
|
||||
badgeDiv = (
|
||||
<div className='TabBar-expired'/>
|
||||
@@ -63,7 +85,6 @@ export default class TabBar extends React.PureComponent { // need "this"
|
||||
draggable={false}
|
||||
ref={id}
|
||||
active={this.props.activeKey === index}
|
||||
activeKey={this.props.activeKey}
|
||||
onMouseDown={() => {
|
||||
this.props.onSelect(team.name, index);
|
||||
}}
|
||||
@@ -98,7 +119,6 @@ export default class TabBar extends React.PureComponent { // need "this"
|
||||
eventKey='addServerButton'
|
||||
draggable={false}
|
||||
title='Add new server'
|
||||
activeKey={this.props.activeKey}
|
||||
onSelect={() => {
|
||||
this.props.onAddServer();
|
||||
}}
|
||||
@@ -111,7 +131,7 @@ export default class TabBar extends React.PureComponent { // need "this"
|
||||
);
|
||||
}
|
||||
|
||||
const navContainer = (ref) => (
|
||||
const navContainer = (ref: React.RefObject<Nav>) => (
|
||||
<Nav
|
||||
ref={ref}
|
||||
className={`smooth-dnd-container TabBar${this.props.isDarkMode ? ' darkMode' : ''}`}
|
||||
@@ -136,18 +156,3 @@ export default class TabBar extends React.PureComponent { // need "this"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TabBar.propTypes = {
|
||||
activeKey: PropTypes.number,
|
||||
id: PropTypes.string,
|
||||
isDarkMode: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
teams: PropTypes.array,
|
||||
sessionsExpired: PropTypes.object,
|
||||
unreadCounts: PropTypes.object,
|
||||
mentionCounts: PropTypes.object,
|
||||
showAddServerButton: PropTypes.bool,
|
||||
onAddServer: PropTypes.func,
|
||||
onDrop: PropTypes.func,
|
||||
tabsDisabled: PropTypes.bool,
|
||||
};
|
@@ -1,17 +1,34 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {ListGroup} from 'react-bootstrap';
|
||||
|
||||
import TeamListItem from './TeamListItem.jsx';
|
||||
import NewTeamModal from './NewTeamModal.jsx';
|
||||
import RemoveServerModal from './RemoveServerModal.jsx';
|
||||
import {Team, TeamWithIndex} from 'types/config';
|
||||
|
||||
export default class TeamList extends React.PureComponent {
|
||||
constructor(props) {
|
||||
import TeamListItem from './TeamListItem';
|
||||
import NewTeamModal from './NewTeamModal';
|
||||
import RemoveServerModal from './RemoveServerModal';
|
||||
|
||||
type Props = {
|
||||
onTeamClick: (teamName: string) => void;
|
||||
onTeamsChange: (teams: Team[]) => void;
|
||||
showAddTeamForm?: boolean;
|
||||
teams: Team[];
|
||||
addServer: (team: Team) => void;
|
||||
updateTeam: (index: number, team: Team) => void;
|
||||
setAddTeamFormVisibility: (visible: boolean) => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
team: TeamWithIndex;
|
||||
showEditTeamForm: boolean;
|
||||
indexToRemoveServer: number;
|
||||
}
|
||||
|
||||
export default class TeamList extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
@@ -20,13 +37,13 @@ export default class TeamList extends React.PureComponent {
|
||||
team: {
|
||||
url: '',
|
||||
name: '',
|
||||
index: false,
|
||||
index: 0,
|
||||
order: props.teams.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
handleTeamRemove = (index) => {
|
||||
handleTeamRemove = (index: number) => {
|
||||
console.log(index);
|
||||
const teams = this.props.teams;
|
||||
const removedOrder = this.props.teams[index].order;
|
||||
@@ -39,7 +56,7 @@ export default class TeamList extends React.PureComponent {
|
||||
this.props.onTeamsChange(teams);
|
||||
}
|
||||
|
||||
handleTeamAdd = (team) => {
|
||||
handleTeamAdd = (team: TeamWithIndex) => {
|
||||
const teams = this.props.teams;
|
||||
|
||||
// check if team already exists and then change existing team or add new one
|
||||
@@ -56,7 +73,7 @@ export default class TeamList extends React.PureComponent {
|
||||
team: {
|
||||
url: '',
|
||||
name: '',
|
||||
index: false,
|
||||
index: 0,
|
||||
order: teams.length,
|
||||
},
|
||||
});
|
||||
@@ -64,7 +81,7 @@ export default class TeamList extends React.PureComponent {
|
||||
this.props.onTeamsChange(teams);
|
||||
}
|
||||
|
||||
openServerRemoveModal = (indexForServer) => {
|
||||
openServerRemoveModal = (indexForServer: number) => {
|
||||
this.setState({indexToRemoveServer: indexForServer});
|
||||
}
|
||||
|
||||
@@ -72,16 +89,16 @@ export default class TeamList extends React.PureComponent {
|
||||
this.setState({indexToRemoveServer: -1});
|
||||
}
|
||||
|
||||
handleTeamRemovePrompt = (index) => {
|
||||
handleTeamRemovePrompt = (index: number) => {
|
||||
return () => {
|
||||
document.activeElement.blur();
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
this.openServerRemoveModal(index);
|
||||
};
|
||||
}
|
||||
|
||||
handleTeamEditing = (team, index) => {
|
||||
handleTeamEditing = (team: Team, index: number) => {
|
||||
return () => {
|
||||
document.activeElement.blur();
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
this.setState({
|
||||
showEditTeamForm: true,
|
||||
team: {
|
||||
@@ -119,7 +136,7 @@ export default class TeamList extends React.PureComponent {
|
||||
team: {
|
||||
name: '',
|
||||
url: '',
|
||||
index: false,
|
||||
index: 0,
|
||||
order: this.props.teams.length,
|
||||
},
|
||||
});
|
||||
@@ -137,12 +154,11 @@ export default class TeamList extends React.PureComponent {
|
||||
this.props.updateTeam(newTeam.index, teamData);
|
||||
}
|
||||
this.setState({
|
||||
showNewTeamModal: false,
|
||||
showEditTeamForm: false,
|
||||
team: {
|
||||
name: '',
|
||||
url: '',
|
||||
index: false,
|
||||
index: 0,
|
||||
order: newTeam.order + 1,
|
||||
},
|
||||
});
|
||||
@@ -175,13 +191,3 @@ export default class TeamList extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TeamList.propTypes = {
|
||||
onTeamClick: PropTypes.func,
|
||||
onTeamsChange: PropTypes.func,
|
||||
showAddTeamForm: PropTypes.bool,
|
||||
teams: PropTypes.array,
|
||||
addServer: PropTypes.func,
|
||||
updateTeam: PropTypes.func,
|
||||
setAddTeamFormVisibility: PropTypes.func,
|
||||
};
|
@@ -1,11 +1,18 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class TeamListItem extends React.PureComponent {
|
||||
type Props = {
|
||||
name: string;
|
||||
onTeamEditing: () => void;
|
||||
onTeamRemove: () => void;
|
||||
onTeamClick: React.MouseEventHandler<HTMLDivElement>;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default class TeamListItem extends React.PureComponent<Props> {
|
||||
handleTeamRemove = () => {
|
||||
this.props.onTeamRemove();
|
||||
}
|
||||
@@ -39,11 +46,3 @@ export default class TeamListItem extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TeamListItem.propTypes = {
|
||||
name: PropTypes.string,
|
||||
onTeamEditing: PropTypes.func,
|
||||
onTeamRemove: PropTypes.func,
|
||||
onTeamClick: PropTypes.func,
|
||||
url: PropTypes.string,
|
||||
};
|
@@ -1,12 +1,17 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import {Button, Navbar, ProgressBar} from 'react-bootstrap';
|
||||
|
||||
function InstallButton(props) {
|
||||
type InstallButtonProps = {
|
||||
notifyOnly?: boolean;
|
||||
onClickInstall?: React.MouseEventHandler<Button>;
|
||||
onClickDownload?: React.MouseEventHandler<Button>;
|
||||
};
|
||||
|
||||
function InstallButton(props: InstallButtonProps) {
|
||||
if (props.notifyOnly) {
|
||||
return (
|
||||
<Button
|
||||
@@ -23,13 +28,20 @@ function InstallButton(props) {
|
||||
);
|
||||
}
|
||||
|
||||
InstallButton.propTypes = {
|
||||
notifyOnly: propTypes.bool.isRequired,
|
||||
onClickInstall: propTypes.func.isRequired,
|
||||
onClickDownload: propTypes.func.isRequired,
|
||||
type UpdaterPageProps = {
|
||||
appName: string;
|
||||
notifyOnly?: boolean;
|
||||
isDownloading?: boolean;
|
||||
progress?: number;
|
||||
onClickInstall?: React.MouseEventHandler<Button>;
|
||||
onClickDownload?: React.MouseEventHandler<Button>;
|
||||
onClickReleaseNotes?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
onClickRemind?: React.MouseEventHandler<Button>;
|
||||
onClickSkip?: React.MouseEventHandler<Button>;
|
||||
onClickCancel?: React.MouseEventHandler<Button>;
|
||||
};
|
||||
|
||||
function UpdaterPage(props) {
|
||||
function UpdaterPage(props: UpdaterPageProps) {
|
||||
let footer;
|
||||
if (props.isDownloading) {
|
||||
footer = (
|
||||
@@ -97,17 +109,4 @@ function UpdaterPage(props) {
|
||||
);
|
||||
}
|
||||
|
||||
UpdaterPage.propTypes = {
|
||||
appName: propTypes.string.isRequired,
|
||||
notifyOnly: propTypes.bool.isRequired,
|
||||
isDownloading: propTypes.bool.isRequired,
|
||||
progress: propTypes.number,
|
||||
onClickInstall: propTypes.func.isRequired,
|
||||
onClickDownload: propTypes.func.isRequired,
|
||||
onClickReleaseNotes: propTypes.func.isRequired,
|
||||
onClickRemind: propTypes.func.isRequired,
|
||||
onClickSkip: propTypes.func.isRequired,
|
||||
onClickCancel: propTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default UpdaterPage;
|
@@ -1,13 +1,13 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import {storiesOf} from '@storybook/react';
|
||||
|
||||
import {action} from '@storybook/addon-actions';
|
||||
|
||||
import UpdaterPage from '../UpdaterPage.jsx';
|
||||
import UpdaterPage from '../UpdaterPage';
|
||||
import '../../css/components/UpdaterPage.css';
|
||||
|
||||
/*
|
@@ -2,16 +2,20 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {Fragment} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal, Button, Row, Col} from 'react-bootstrap';
|
||||
import {Certificate} from 'electron/renderer';
|
||||
|
||||
export default class ShowCertificateModal extends React.PureComponent {
|
||||
static propTypes = {
|
||||
certificate: PropTypes.object,
|
||||
onOk: PropTypes.func.isRequired,
|
||||
};
|
||||
type Props = {
|
||||
certificate: Certificate;
|
||||
onOk: () => void;
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
type State = {
|
||||
certificate?: Certificate;
|
||||
}
|
||||
|
||||
export default class ShowCertificateModal extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
certificate: props.certificate,
|
||||
@@ -19,12 +23,12 @@ export default class ShowCertificateModal extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleOk = () => {
|
||||
this.setState({certificate: null});
|
||||
this.setState({certificate: undefined});
|
||||
this.props.onOk();
|
||||
}
|
||||
|
||||
render() {
|
||||
const certificateSection = (descriptor) => {
|
||||
const certificateSection = (descriptor: React.ReactNode) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<dt className={'certificate-key'}>{descriptor}</dt>
|
||||
@@ -32,7 +36,7 @@ export default class ShowCertificateModal extends React.PureComponent {
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
const certificateItem = (descriptor, value) => {
|
||||
const certificateItem = (descriptor: React.ReactNode, value: React.ReactNode) => {
|
||||
const val = value ? `${value}` : <span/>;
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -47,6 +51,7 @@ export default class ShowCertificateModal extends React.PureComponent {
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='show-certificate'
|
||||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Body>
|
||||
{'No certificate Selected'}
|
||||
@@ -55,22 +60,22 @@ export default class ShowCertificateModal extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
const utcSeconds = (date) => {
|
||||
const utcSeconds = (date: number) => {
|
||||
const d = new Date(0);
|
||||
d.setUTCSeconds(date);
|
||||
return d;
|
||||
};
|
||||
|
||||
const expiration = utcSeconds(this.state.certificate.validExpiry);
|
||||
const creation = utcSeconds(this.state.certificate.validStart);
|
||||
const dateDisplayOptions = {dateStyle: 'full', timeStyle: 'full'};
|
||||
const expiration = utcSeconds(this.state.certificate?.validExpiry || 0);
|
||||
const creation = utcSeconds(this.state.certificate?.validStart || 0);
|
||||
const dateDisplayOptions = {dateStyle: 'full' as const, timeStyle: 'full' as const};
|
||||
const dateLocale = 'en-US';
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='show-certificate'
|
||||
show={this.state.certificate !== null}
|
||||
scrollable={'true'}
|
||||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Header className={'no-border'}>
|
||||
<Modal.Title>{'Certificate information'}</Modal.Title>
|
||||
@@ -79,20 +84,20 @@ export default class ShowCertificateModal extends React.PureComponent {
|
||||
<p className='details'>{'Details'}</p>
|
||||
<dl>
|
||||
{certificateSection('Subject Name')}
|
||||
{certificateItem('Common Name', this.state.certificate.subject.commonName)}
|
||||
{certificateItem('Common Name', this.state.certificate?.subject.commonName)}
|
||||
</dl>
|
||||
<dl>
|
||||
{certificateSection('Issuer Name')}
|
||||
{certificateItem('Common Name', this.state.certificate.issuer.commonName)}
|
||||
{certificateItem('Common Name', this.state.certificate?.issuer.commonName)}
|
||||
</dl>
|
||||
<dl>
|
||||
{certificateItem('Serial Number', this.state.certificate.serialNumber)}
|
||||
{certificateItem('Serial Number', this.state.certificate?.serialNumber)}
|
||||
{certificateItem('Not Valid Before', creation.toLocaleString(dateLocale, dateDisplayOptions))}
|
||||
{certificateItem('Not Valid After', expiration.toLocaleString(dateLocale, dateDisplayOptions))}
|
||||
</dl>
|
||||
<dl>
|
||||
{certificateSection('Public Key Info')}
|
||||
{certificateItem('Algorithm', this.state.certificate.fingerprint.split('/')[0])}
|
||||
{certificateItem('Algorithm', this.state.certificate?.fingerprint.split('/')[0])}
|
||||
</dl>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={'no-border'}>
|
||||
@@ -100,7 +105,6 @@ export default class ShowCertificateModal extends React.PureComponent {
|
||||
<Row>
|
||||
<Col>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
onClick={this.handleOk}
|
||||
className={'primary'}
|
||||
>{'Close'}</Button>
|
@@ -2,9 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
|
||||
export default function UrlDescription(props) {
|
||||
export default function UrlDescription(props: {url: string}) {
|
||||
if (props.url) {
|
||||
return (
|
||||
<div className='HoveringURL HoveringURL-left'>
|
||||
@@ -12,8 +11,6 @@ export default function UrlDescription(props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UrlDescription.propTypes = {
|
||||
url: propTypes.string,
|
||||
};
|
||||
return null;
|
||||
}
|
@@ -12,18 +12,18 @@ import React from 'react';
|
||||
* bubbled from all descendent elements but when false, only listens for events coming from the target element and
|
||||
* ignores events bubbling up from descendent elements
|
||||
*/
|
||||
function useAnimationEnd(
|
||||
ref,
|
||||
callback,
|
||||
animationName,
|
||||
function useAnimationEnd<T extends Element>(
|
||||
ref: React.RefObject<T>,
|
||||
callback: (event: Event) => void,
|
||||
animationName: string,
|
||||
listenForEventBubbling = true,
|
||||
) {
|
||||
): void {
|
||||
React.useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function handleAnimationend(event) {
|
||||
function handleAnimationend(event: Event & {animationName?: string}) {
|
||||
if (!listenForEventBubbling && event.target !== ref.current) {
|
||||
return;
|
||||
}
|
||||
@@ -36,7 +36,7 @@ function useAnimationEnd(
|
||||
ref.current.addEventListener('animationend', handleAnimationend);
|
||||
|
||||
return () => {
|
||||
ref.current.removeEventListener('animationend', handleAnimationend);
|
||||
ref.current?.removeEventListener('animationend', handleAnimationend);
|
||||
};
|
||||
}, [ref, callback, animationName, listenForEventBubbling]);
|
||||
}
|
@@ -12,10 +12,10 @@ import React from 'react';
|
||||
* bubbled from all descendent elements but when false, only listens for events coming from the target element and
|
||||
* ignores events bubbling up from descendent elements
|
||||
*/
|
||||
function useTransitionend(
|
||||
ref,
|
||||
callback,
|
||||
properties,
|
||||
function useTransitionend<T extends Element>(
|
||||
ref: React.RefObject<T>,
|
||||
callback: (event: Event) => void,
|
||||
properties: string[],
|
||||
listenForEventBubbling = true,
|
||||
) {
|
||||
React.useEffect(() => {
|
||||
@@ -23,7 +23,7 @@ function useTransitionend(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function handleTransitionEnd(event) {
|
||||
function handleTransitionEnd(event: Event & {propertyName?: string}) {
|
||||
if (!listenForEventBubbling && event.target !== ref.current) {
|
||||
return;
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/index.css';
|
||||
@@ -8,11 +8,18 @@ import 'renderer/css/index.css';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {CombinedConfig, Team} from 'types/config';
|
||||
|
||||
import {GET_CONFIGURATION, UPDATE_TEAMS, QUIT, RELOAD_CONFIGURATION} from 'common/communication';
|
||||
|
||||
import MainPage from './components/MainPage.jsx';
|
||||
class Root extends React.PureComponent {
|
||||
constructor(props) {
|
||||
import MainPage from './components/MainPage';
|
||||
|
||||
type State = {
|
||||
config?: CombinedConfig;
|
||||
}
|
||||
|
||||
class Root extends React.PureComponent<Record<string, never>, State> {
|
||||
constructor(props: Record<string, never>) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
@@ -39,7 +46,10 @@ class Root extends React.PureComponent {
|
||||
this.setState({config});
|
||||
}
|
||||
|
||||
moveTabs = async (originalOrder, newOrder) => {
|
||||
moveTabs = async (originalOrder: number, newOrder: number): Promise<number | undefined> => {
|
||||
if (!this.state.config) {
|
||||
throw new Error('No config');
|
||||
}
|
||||
const teams = this.state.config.teams.concat();
|
||||
const tabOrder = teams.map((team, index) => {
|
||||
return {
|
||||
@@ -62,12 +72,9 @@ class Root extends React.PureComponent {
|
||||
return teamIndex;
|
||||
};
|
||||
|
||||
teamConfigChange = async (updatedTeams, callback) => {
|
||||
const updatedConfig = await window.ipcRenderer.invoke(UPDATE_TEAMS, updatedTeams);
|
||||
teamConfigChange = async (updatedTeams: Team[]) => {
|
||||
await window.ipcRenderer.invoke(UPDATE_TEAMS, updatedTeams);
|
||||
await this.reloadConfig();
|
||||
if (callback) {
|
||||
callback(updatedConfig);
|
||||
}
|
||||
};
|
||||
|
||||
reloadConfig = async () => {
|
||||
@@ -75,7 +82,7 @@ class Root extends React.PureComponent {
|
||||
this.setState({config});
|
||||
};
|
||||
|
||||
requestConfig = async (exitOnError) => {
|
||||
requestConfig = async (exitOnError?: boolean) => {
|
||||
// todo: should we block?
|
||||
try {
|
||||
const configRequest = await window.ipcRenderer.invoke(GET_CONFIGURATION);
|
||||
@@ -115,6 +122,8 @@ class Root extends React.PureComponent {
|
||||
}
|
||||
window.ipcRenderer.invoke('get-app-version').then(({name, version}) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
console.log(`Starting ${name} v${version} commit: ${__HASH_VERSION__}`);
|
||||
});
|
||||
|
@@ -3,10 +3,11 @@
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {Certificate} from 'electron/renderer';
|
||||
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication.js';
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication';
|
||||
|
||||
import SelectCertificateModal from './certificateModal.jsx';
|
||||
import SelectCertificateModal from './certificateModal';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
@@ -16,7 +17,7 @@ const handleCancel = () => {
|
||||
window.postMessage({type: MODAL_CANCEL}, window.location.href);
|
||||
};
|
||||
|
||||
const handleSelect = (cert) => {
|
||||
const handleSelect = (cert: Certificate) => {
|
||||
window.postMessage({type: MODAL_RESULT, data: {cert}}, window.location.href);
|
||||
};
|
||||
|
@@ -1,27 +1,34 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Certificate} from 'electron/renderer';
|
||||
import React, {Fragment} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal, Button, Table, Row, Col} from 'react-bootstrap';
|
||||
|
||||
import {CertificateModalData} from 'types/certificate';
|
||||
import {ModalMessage} from 'types/modals';
|
||||
|
||||
import {MODAL_INFO} from 'common/communication';
|
||||
|
||||
import ShowCertificateModal from '../../components/showCertificateModal.jsx';
|
||||
import ShowCertificateModal from '../../components/showCertificateModal';
|
||||
|
||||
export default class SelectCertificateModal extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
getCertInfo: PropTypes.func,
|
||||
}
|
||||
type Props = {
|
||||
onSelect: (cert: Certificate) => void;
|
||||
onCancel?: () => void;
|
||||
getCertInfo: () => void;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
type State = {
|
||||
selectedIndex?: number;
|
||||
showCertificate?: Certificate;
|
||||
url?: string;
|
||||
list?: Certificate[];
|
||||
}
|
||||
|
||||
export default class SelectCertificateModal extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedIndex: null,
|
||||
showCertificate: null,
|
||||
};
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -34,7 +41,7 @@ export default class SelectCertificateModal extends React.PureComponent {
|
||||
window.removeEventListener('message', this.handleCertInfoMessage);
|
||||
}
|
||||
|
||||
handleCertInfoMessage = (event) => {
|
||||
handleCertInfoMessage = (event: {data: ModalMessage<CertificateModalData>}) => {
|
||||
switch (event.data.type) {
|
||||
case MODAL_INFO: {
|
||||
const {url, list} = event.data.data;
|
||||
@@ -46,13 +53,13 @@ export default class SelectCertificateModal extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
selectfn = (index) => {
|
||||
selectfn = (index: number) => {
|
||||
return (() => {
|
||||
this.setState({selectedIndex: index});
|
||||
});
|
||||
};
|
||||
|
||||
renderCert = (cert, index) => {
|
||||
renderCert = (cert: Certificate, index: number) => {
|
||||
const issuer = (cert.issuerName || (cert.issuer && cert.issuer.commonName) || '');
|
||||
const subject = (cert.subjectName || (cert.subject && cert.subject.commonName) || '');
|
||||
const serial = cert.serialNumber || '';
|
||||
@@ -75,7 +82,7 @@ export default class SelectCertificateModal extends React.PureComponent {
|
||||
</tr>);
|
||||
};
|
||||
|
||||
renderCerts = (certificateList) => {
|
||||
renderCerts = (certificateList: Certificate[]) => {
|
||||
if (certificateList) {
|
||||
const certs = certificateList.map(this.renderCert);
|
||||
return (
|
||||
@@ -88,12 +95,15 @@ export default class SelectCertificateModal extends React.PureComponent {
|
||||
}
|
||||
|
||||
getSelectedCert = () => {
|
||||
return this.state.selectedIndex === null ? null : this.state.list[this.state.selectedIndex];
|
||||
if (this.state.list && this.state.selectedIndex) {
|
||||
return this.state.list[this.state.selectedIndex];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
handleOk = () => {
|
||||
const cert = this.getSelectedCert();
|
||||
if (cert !== null) {
|
||||
if (cert) {
|
||||
this.props.onSelect(cert);
|
||||
}
|
||||
}
|
||||
@@ -104,7 +114,7 @@ export default class SelectCertificateModal extends React.PureComponent {
|
||||
}
|
||||
|
||||
certificateInfoClose = () => {
|
||||
this.setState({showCertificate: null});
|
||||
this.setState({showCertificate: undefined});
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -122,6 +132,7 @@ export default class SelectCertificateModal extends React.PureComponent {
|
||||
bsClass='modal'
|
||||
className='certificate-modal'
|
||||
show={Boolean(this.state.list && this.state.url)}
|
||||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title >{'Select a certificate'}</Modal.Title>
|
||||
@@ -131,7 +142,6 @@ export default class SelectCertificateModal extends React.PureComponent {
|
||||
<Table
|
||||
striped={true}
|
||||
hover={true}
|
||||
size={'sm'}
|
||||
responsive={true}
|
||||
className='certificate-list'
|
||||
tabIndex={1}
|
||||
@@ -144,7 +154,7 @@ export default class SelectCertificateModal extends React.PureComponent {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderCerts(this.state.list)}
|
||||
{this.renderCerts(this.state.list!)}
|
||||
<tr/* this is to correct table height without affecting real rows *//>
|
||||
</tbody>
|
||||
</Table>
|
||||
@@ -154,7 +164,6 @@ export default class SelectCertificateModal extends React.PureComponent {
|
||||
<Row>
|
||||
<Col sm={4}>
|
||||
<Button
|
||||
variant={'info'}
|
||||
disabled={this.state.selectedIndex === null}
|
||||
onClick={this.handleCertificateInfo}
|
||||
className={'info'}
|
||||
@@ -163,11 +172,9 @@ export default class SelectCertificateModal extends React.PureComponent {
|
||||
<Col sm={8}>
|
||||
<Button
|
||||
onClick={this.props.onCancel}
|
||||
variant={'secondary'}
|
||||
className={'secondary'}
|
||||
>{'Cancel'}</Button>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
onClick={this.handleOk}
|
||||
disabled={this.state.selectedIndex === null}
|
||||
className={'primary'}
|
@@ -4,18 +4,27 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {RECEIVED_LOADING_SCREEN_DATA, GET_LOADING_SCREEN_DATA, LOADING_SCREEN_ANIMATION_FINISHED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication.js';
|
||||
import {ModalMessage} from 'types/modals';
|
||||
|
||||
import LoadingScreen from '../../components/LoadingScreen.jsx';
|
||||
import {RECEIVED_LOADING_SCREEN_DATA, GET_LOADING_SCREEN_DATA, LOADING_SCREEN_ANIMATION_FINISHED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication';
|
||||
|
||||
import LoadingScreen from '../../components/LoadingScreen';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
import 'renderer/css/components/LoadingAnimation.css';
|
||||
import 'renderer/css/components/LoadingScreen.css';
|
||||
|
||||
class LoadingScreenRoot extends React.PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
type Props = Record<string, never>;
|
||||
|
||||
type State = {
|
||||
showLoadingScreen: boolean;
|
||||
darkMode: boolean;
|
||||
}
|
||||
|
||||
class LoadingScreenRoot extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showLoadingScreen: true,
|
||||
darkMode: false,
|
||||
@@ -32,7 +41,7 @@ class LoadingScreenRoot extends React.PureComponent {
|
||||
window.removeEventListener('message', this.handleMessageEvent);
|
||||
}
|
||||
|
||||
handleMessageEvent = (event) => {
|
||||
handleMessageEvent = (event: {data: ModalMessage<any>}) => {
|
||||
if (event.data.type === RECEIVED_LOADING_SCREEN_DATA) {
|
||||
this.setState({
|
||||
darkMode: event.data.data.darkMode,
|
@@ -3,19 +3,20 @@
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {AuthenticationResponseDetails} from 'electron/renderer';
|
||||
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication.js';
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication';
|
||||
|
||||
import LoginModal from './loginModal.jsx';
|
||||
import LoginModal from './loginModal';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
|
||||
const handleLoginCancel = (request) => {
|
||||
const handleLoginCancel = (request: AuthenticationResponseDetails) => {
|
||||
window.postMessage({type: MODAL_CANCEL, data: {request}}, window.location.href);
|
||||
};
|
||||
|
||||
const handleLogin = (request, username, password) => {
|
||||
const handleLogin = (request: AuthenticationResponseDetails, username: string, password: string) => {
|
||||
window.postMessage({type: MODAL_RESULT, data: {request, username, password}}, window.location.href);
|
||||
};
|
||||
|
@@ -3,20 +3,34 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Col, ControlLabel, Form, FormGroup, FormControl, Modal} from 'react-bootstrap';
|
||||
|
||||
import {MODAL_INFO} from 'common/communication';
|
||||
import urlUtils from 'common/utils/url';
|
||||
import {LoginModalData} from 'types/auth';
|
||||
import {ModalMessage} from 'types/modals';
|
||||
import {AuthenticationResponseDetails, AuthInfo} from 'electron/renderer';
|
||||
|
||||
export default class LoginModal extends React.PureComponent {
|
||||
constructor(props) {
|
||||
import urlUtils from 'common/utils/url';
|
||||
import {MODAL_INFO} from 'common/communication';
|
||||
|
||||
type Props = {
|
||||
onCancel: (request: AuthenticationResponseDetails) => void;
|
||||
onLogin: (request: AuthenticationResponseDetails, username: string, password: string) => void;
|
||||
getAuthInfo: () => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
username: string;
|
||||
password: string;
|
||||
request?: AuthenticationResponseDetails;
|
||||
authInfo?: AuthInfo;
|
||||
};
|
||||
|
||||
export default class LoginModal extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: '',
|
||||
password: '',
|
||||
request: null,
|
||||
authInfo: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,7 +44,7 @@ export default class LoginModal extends React.PureComponent {
|
||||
window.removeEventListener('message', this.handleAuthInfoMessage);
|
||||
}
|
||||
|
||||
handleAuthInfoMessage = (event) => {
|
||||
handleAuthInfoMessage = (event: {data: ModalMessage<LoginModalData>}) => {
|
||||
switch (event.data.type) {
|
||||
case MODAL_INFO: {
|
||||
const {request, authInfo} = event.data.data;
|
||||
@@ -42,33 +56,33 @@ export default class LoginModal extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit = (event) => {
|
||||
handleSubmit = (event: React.MouseEvent<Button>) => {
|
||||
event.preventDefault();
|
||||
this.props.onLogin(this.state.request, this.state.username, this.state.password);
|
||||
this.props.onLogin(this.state.request!, this.state.username, this.state.password);
|
||||
this.setState({
|
||||
username: '',
|
||||
password: '',
|
||||
request: null,
|
||||
authInfo: null,
|
||||
request: undefined,
|
||||
authInfo: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
handleCancel = (event) => {
|
||||
handleCancel = (event: React.MouseEvent<Button>) => {
|
||||
event.preventDefault();
|
||||
this.props.onCancel(this.state.request);
|
||||
this.props.onCancel(this.state.request!);
|
||||
this.setState({
|
||||
username: '',
|
||||
password: '',
|
||||
request: null,
|
||||
authInfo: null,
|
||||
request: undefined,
|
||||
authInfo: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
setUsername = (e) => {
|
||||
setUsername = (e: React.ChangeEvent<FormControl & HTMLInputElement>) => {
|
||||
this.setState({username: e.target.value});
|
||||
}
|
||||
|
||||
setPassword = (e) => {
|
||||
setPassword = (e: React.ChangeEvent<FormControl & HTMLInputElement>) => {
|
||||
this.setState({password: e.target.value});
|
||||
}
|
||||
|
||||
@@ -80,11 +94,14 @@ export default class LoginModal extends React.PureComponent {
|
||||
theServer = `The proxy ${this.state.authInfo.host}:${this.state.authInfo.port}`;
|
||||
} else {
|
||||
const tmpURL = urlUtils.parseURL(this.state.request.url);
|
||||
theServer = `The server ${tmpURL.protocol}//${tmpURL.host}`;
|
||||
theServer = `The server ${tmpURL?.protocol}//${tmpURL?.host}`;
|
||||
}
|
||||
const message = `${theServer} requires a username and password.`;
|
||||
return (
|
||||
<Modal show={Boolean(this.state.request && this.state.authInfo)}>
|
||||
<Modal
|
||||
show={Boolean(this.state.request && this.state.authInfo)}
|
||||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{'Authentication Required'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
@@ -148,9 +165,3 @@ export default class LoginModal extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoginModal.propTypes = {
|
||||
onCancel: PropTypes.func,
|
||||
onLogin: PropTypes.func,
|
||||
getAuthInfo: PropTypes.func,
|
||||
};
|
@@ -4,21 +4,20 @@
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {MODAL_CANCEL, MODAL_RESULT} from 'common/communication.js';
|
||||
import {TeamWithIndex} from 'types/config';
|
||||
|
||||
import NewTeamModal from '../../components/NewTeamModal.jsx'; //'./addServer.jsx';
|
||||
import {MODAL_CANCEL, MODAL_RESULT} from 'common/communication';
|
||||
|
||||
import NewTeamModal from '../../components/NewTeamModal'; //'./addServer.jsx';
|
||||
|
||||
const onClose = () => {
|
||||
window.postMessage({type: MODAL_CANCEL}, window.location.href);
|
||||
};
|
||||
|
||||
const onSave = (data) => {
|
||||
const onSave = (data: TeamWithIndex) => {
|
||||
window.postMessage({type: MODAL_RESULT, data}, window.location.href);
|
||||
};
|
||||
|
||||
@@ -29,7 +28,6 @@ const start = async () => {
|
||||
onSave={onSave}
|
||||
editMode={false}
|
||||
show={true}
|
||||
url={decodeURIComponent(urlParams.get('url'))}
|
||||
/>,
|
||||
document.getElementById('app'),
|
||||
);
|
@@ -4,9 +4,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO, MODAL_SEND_IPC_MESSAGE} from 'common/communication.js';
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO, MODAL_SEND_IPC_MESSAGE} from 'common/communication';
|
||||
|
||||
import PermissionModal from './permissionModal.jsx';
|
||||
import PermissionModal from './permissionModal';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
@@ -23,7 +23,7 @@ const getPermissionInfo = () => {
|
||||
window.postMessage({type: RETRIEVE_MODAL_INFO}, window.location.href);
|
||||
};
|
||||
|
||||
const openExternalLink = (protocol, url) => {
|
||||
const openExternalLink = (protocol: string, url: string) => {
|
||||
window.postMessage({type: MODAL_SEND_IPC_MESSAGE, data: {type: 'confirm-protocol', args: [protocol, url]}}, window.location.href);
|
||||
};
|
||||
|
@@ -3,14 +3,29 @@
|
||||
|
||||
import React from 'react';
|
||||
import {Modal, Button} from 'react-bootstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {PermissionType} from 'types/trustedOrigin';
|
||||
|
||||
import {ModalMessage} from 'types/modals';
|
||||
|
||||
import urlUtil from 'common/utils/url';
|
||||
import {MODAL_INFO} from 'common/communication';
|
||||
import {PERMISSION_DESCRIPTION} from 'common/permissions';
|
||||
|
||||
export default class PermissionModal extends React.PureComponent {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
handleDeny: React.MouseEventHandler<Button>;
|
||||
handleGrant: React.MouseEventHandler<Button>;
|
||||
getPermissionInfo: () => void;
|
||||
openExternalLink: (protocol: string, url: string) => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
url?: string;
|
||||
permission?: PermissionType;
|
||||
}
|
||||
|
||||
export default class PermissionModal extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
@@ -25,7 +40,7 @@ export default class PermissionModal extends React.PureComponent {
|
||||
window.removeEventListener('message', this.handlePermissionInfoMessage);
|
||||
}
|
||||
|
||||
handlePermissionInfoMessage = (event) => {
|
||||
handlePermissionInfoMessage = (event: {data: ModalMessage<{url: string; permission: PermissionType}>}) => {
|
||||
switch (event.data.type) {
|
||||
case MODAL_INFO: {
|
||||
const {url, permission} = event.data.data;
|
||||
@@ -38,7 +53,7 @@ export default class PermissionModal extends React.PureComponent {
|
||||
}
|
||||
|
||||
getModalTitle() {
|
||||
return `${PERMISSION_DESCRIPTION[this.state.permission]} Required`;
|
||||
return `${PERMISSION_DESCRIPTION[this.state.permission!]} Required`;
|
||||
}
|
||||
|
||||
getModalBody() {
|
||||
@@ -46,12 +61,12 @@ export default class PermissionModal extends React.PureComponent {
|
||||
const originDisplay = url ? urlUtil.getHost(url) : 'unknown origin';
|
||||
const originLink = url ? originDisplay : '';
|
||||
|
||||
const click = (e) => {
|
||||
const click = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
let parseUrl;
|
||||
try {
|
||||
parseUrl = urlUtil.parseURL(originLink);
|
||||
this.props.openExternalLink(parseUrl.protocol, originLink);
|
||||
this.props.openExternalLink(parseUrl!.protocol, originLink);
|
||||
} catch (err) {
|
||||
console.error(`invalid url ${originLink} supplied to externallink: ${err}`);
|
||||
}
|
||||
@@ -60,7 +75,7 @@ export default class PermissionModal extends React.PureComponent {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
{`A site that's not included in your Mattermost server configuration requires access for ${PERMISSION_DESCRIPTION[permission]}.`}
|
||||
{`A site that's not included in your Mattermost server configuration requires access for ${PERMISSION_DESCRIPTION[permission!]}.`}
|
||||
</p>
|
||||
<p>
|
||||
<span>{'This request originated from '}</span>
|
||||
@@ -78,6 +93,7 @@ export default class PermissionModal extends React.PureComponent {
|
||||
show={Boolean(this.state.url && this.state.permission)}
|
||||
id='requestPermissionModal'
|
||||
enforceFocus={true}
|
||||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{this.getModalTitle()}</Modal.Title>
|
||||
@@ -100,10 +116,3 @@ export default class PermissionModal extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PermissionModal.propTypes = {
|
||||
handleDeny: PropTypes.func,
|
||||
handleGrant: PropTypes.func,
|
||||
getPermissionInfo: PropTypes.func,
|
||||
openExternalLink: PropTypes.func,
|
||||
};
|
@@ -9,12 +9,12 @@ const urlParams = new URLSearchParams(queryString);
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import UrlDescription from '../../components/urlDescription.jsx';
|
||||
import UrlDescription from '../../components/urlDescription';
|
||||
|
||||
const start = async () => {
|
||||
ReactDOM.render(
|
||||
<UrlDescription
|
||||
url={decodeURIComponent(urlParams.get('url'))}
|
||||
url={decodeURIComponent(urlParams.get('url')!)}
|
||||
/>,
|
||||
document.getElementById('app'),
|
||||
);
|
@@ -22,7 +22,7 @@ const notificationSounds = new Map([
|
||||
['Upstairs', upstairs],
|
||||
]);
|
||||
|
||||
export const playSound = throttle((soundName) => {
|
||||
export const playSound = throttle((soundName: string) => {
|
||||
if (soundName) {
|
||||
const audio = new Audio(notificationSounds.get(soundName));
|
||||
audio.play();
|
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/index.css';
|
||||
@@ -9,19 +9,11 @@ import 'renderer/css/settings.css';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import SettingsPage from './components/SettingsPage.jsx';
|
||||
|
||||
function openMenu() {
|
||||
if (window.process.platform !== 'darwin') {
|
||||
window.ipcRenderer.send('open-app-menu');
|
||||
}
|
||||
}
|
||||
import SettingsPage from './components/SettingsPage';
|
||||
|
||||
const start = async () => {
|
||||
ReactDOM.render(
|
||||
<SettingsPage
|
||||
openMenu={openMenu}
|
||||
/>,
|
||||
<SettingsPage/>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
};
|
@@ -1,21 +1,29 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import url from 'url';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import propTypes from 'prop-types';
|
||||
import {remote} from 'electron';
|
||||
|
||||
import UpdaterPage from './components/UpdaterPage.jsx';
|
||||
import UpdaterPage from './components/UpdaterPage';
|
||||
|
||||
const thisURL = url.parse(location.href, true);
|
||||
const notifyOnly = thisURL.query.notifyOnly === 'true';
|
||||
|
||||
class UpdaterPageContainer extends React.PureComponent {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
notifyOnly: boolean;
|
||||
initialState: State;
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
}
|
||||
|
||||
class UpdaterPageContainer extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = props.initialState;
|
||||
}
|
||||
@@ -143,11 +151,6 @@ class UpdaterPageContainer extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
UpdaterPageContainer.propTypes = {
|
||||
notifyOnly: propTypes.bool,
|
||||
initialState: propTypes.object,
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<UpdaterPageContainer
|
||||
notifyOnly={notifyOnly}
|
Reference in New Issue
Block a user