[MM-39491] Force Add Server modal to stay until server has been added (#1869)

* [MM-39491] Force Add Server modal to stay until server has been added

* Make the parameter optional

* Actually do the logic, add a test for the logic

* Add remove event listener
This commit is contained in:
Devin Binnie 2021-11-25 09:47:20 -05:00 committed by GitHub
parent b4903d24c9
commit a4a275bd73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 122 additions and 30 deletions

View file

@ -4,6 +4,8 @@
'use strict'; 'use strict';
const robot = require('robotjs');
const env = require('../../modules/environment'); const env = require('../../modules/environment');
describe('startup/app', function desc() { describe('startup/app', function desc() {
@ -27,6 +29,17 @@ describe('startup/app', function desc() {
modalTitle.should.equal('Add Server'); modalTitle.should.equal('Add Server');
}); });
it('MM-T4419 should not allow the user to close the new server modal when no servers exist', async () => {
const newServerModal = this.app.windows().find((window) => window.url().includes('newServer'));
const existing = await newServerModal.isVisible('#cancelNewServerModal');
existing.should.be.false;
robot.keyTap('escape');
const existingModal = this.app.windows().find((window) => window.url().includes('newServer'));
existingModal.should.not.be.null;
});
it('MM-T4399_2 should show no servers configured in dropdown when no servers exist', async () => { it('MM-T4399_2 should show no servers configured in dropdown when no servers exist', async () => {
const mainWindow = this.app.windows().find((window) => window.url().includes('index')); const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
const dropdownButtonText = await mainWindow.innerText('.TeamDropdownButton'); const dropdownButtonText = await mainWindow.innerText('.TeamDropdownButton');

View file

@ -104,3 +104,6 @@ export const GET_AVAILABLE_SPELL_CHECKER_LANGUAGES = 'get-available-spell-checke
export const GET_VIEW_NAME = 'get-view-name'; export const GET_VIEW_NAME = 'get-view-name';
export const GET_VIEW_WEBCONTENTS_ID = 'get-view-webcontents-id'; export const GET_VIEW_WEBCONTENTS_ID = 'get-view-webcontents-id';
export const GET_MODAL_UNCLOSEABLE = 'get-modal-uncloseable';
export const MODAL_UNCLOSEABLE = 'modal-uncloseable';

View file

@ -554,7 +554,7 @@ function handleNewServerModal() {
if (!mainWindow) { if (!mainWindow) {
return; return;
} }
const modalPromise = addModal<unknown, Team>('newServer', html, modalPreload, {}, mainWindow); const modalPromise = addModal<unknown, Team>('newServer', html, modalPreload, {}, mainWindow, config.teams.length === 0);
if (modalPromise) { if (modalPromise) {
modalPromise.then((data) => { modalPromise.then((data) => {
const teams = config.teams; const teams = config.teams;

View file

@ -6,10 +6,29 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import {MODAL_CANCEL, MODAL_RESULT, MODAL_INFO, RETRIEVE_MODAL_INFO, MODAL_SEND_IPC_MESSAGE, GET_DARK_MODE, DARK_MODE_CHANGE} from 'common/communication'; import {
MODAL_CANCEL,
MODAL_RESULT,
MODAL_INFO,
RETRIEVE_MODAL_INFO,
MODAL_SEND_IPC_MESSAGE,
GET_DARK_MODE,
DARK_MODE_CHANGE,
GET_MODAL_UNCLOSEABLE,
MODAL_UNCLOSEABLE,
} from 'common/communication';
console.log('preloaded for the modal!'); console.log('preloaded for the modal!');
let uncloseable = false;
const createKeyDownListener = () => {
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !uncloseable) {
ipcRenderer.send(MODAL_CANCEL);
}
});
};
window.addEventListener('message', async (event) => { window.addEventListener('message', async (event) => {
switch (event.data.type) { switch (event.data.type) {
case MODAL_CANCEL: { case MODAL_CANCEL: {
@ -26,6 +45,12 @@ window.addEventListener('message', async (event) => {
console.log('getting modal data'); console.log('getting modal data');
window.postMessage({type: MODAL_INFO, data: await ipcRenderer.invoke(RETRIEVE_MODAL_INFO)}, window.location.href); window.postMessage({type: MODAL_INFO, data: await ipcRenderer.invoke(RETRIEVE_MODAL_INFO)}, window.location.href);
break; break;
case GET_MODAL_UNCLOSEABLE:
console.log('get modal uncloseable');
uncloseable = await ipcRenderer.invoke(GET_MODAL_UNCLOSEABLE);
createKeyDownListener();
window.postMessage({type: MODAL_UNCLOSEABLE, data: uncloseable}, window.location.href);
break;
case MODAL_SEND_IPC_MESSAGE: case MODAL_SEND_IPC_MESSAGE:
console.log('sending custom ipc message'); console.log('sending custom ipc message');
ipcRenderer.send(event.data.data.type, ...event.data.data.args); ipcRenderer.send(event.data.data.type, ...event.data.data.args);
@ -40,11 +65,7 @@ window.addEventListener('message', async (event) => {
} }
}); });
window.addEventListener('keydown', (e) => { createKeyDownListener();
if (e.key === 'Escape') {
ipcRenderer.send(MODAL_CANCEL);
}
});
ipcRenderer.on(DARK_MODE_CHANGE, (event, darkMode) => { ipcRenderer.on(DARK_MODE_CHANGE, (event, darkMode) => {
window.postMessage({type: DARK_MODE_CHANGE, data: darkMode}, window.location.href); window.postMessage({type: DARK_MODE_CHANGE, data: darkMode}, window.location.href);

View file

@ -6,7 +6,16 @@ import {IpcMainEvent, IpcMainInvokeEvent} from 'electron/main';
import {CombinedConfig} from 'types/config'; import {CombinedConfig} from 'types/config';
import {RETRIEVE_MODAL_INFO, MODAL_CANCEL, MODAL_RESULT, MODAL_OPEN, MODAL_CLOSE, EMIT_CONFIGURATION, DARK_MODE_CHANGE} from 'common/communication'; import {
RETRIEVE_MODAL_INFO,
MODAL_CANCEL,
MODAL_RESULT,
MODAL_OPEN,
MODAL_CLOSE,
EMIT_CONFIGURATION,
DARK_MODE_CHANGE,
GET_MODAL_UNCLOSEABLE,
} from 'common/communication';
import * as WindowManager from '../windows/windowManager'; import * as WindowManager from '../windows/windowManager';
@ -16,11 +25,11 @@ let modalQueue: Array<ModalView<any, any>> = [];
const modalPromises: Map<string, Promise<any>> = new Map(); const modalPromises: Map<string, Promise<any>> = new Map();
// TODO: add a queue/add differentiation, in case we need to put a modal first in line // TODO: add a queue/add differentiation, in case we need to put a modal first in line
export function addModal<T, T2>(key: string, html: string, preload: string, data: T, win: BrowserWindow) { export function addModal<T, T2>(key: string, html: string, preload: string, data: T, win: BrowserWindow, uncloseable = false) {
const foundModal = modalQueue.find((modal) => modal.key === key); const foundModal = modalQueue.find((modal) => modal.key === key);
if (!foundModal) { if (!foundModal) {
const modalPromise = new Promise((resolve: (value: T2) => void, reject) => { const modalPromise = new Promise((resolve: (value: T2) => void, reject) => {
const mv = new ModalView<T, T2>(key, html, preload, data, resolve, reject, win); const mv = new ModalView<T, T2>(key, html, preload, data, resolve, reject, win, uncloseable);
modalQueue.push(mv); modalQueue.push(mv);
}); });
@ -34,6 +43,7 @@ export function addModal<T, T2>(key: string, html: string, preload: string, data
return modalPromises.get(key) as Promise<T2>; return modalPromises.get(key) as Promise<T2>;
} }
ipcMain.handle(GET_MODAL_UNCLOSEABLE, handleGetModalUncloseable);
ipcMain.handle(RETRIEVE_MODAL_INFO, handleInfoRequest); ipcMain.handle(RETRIEVE_MODAL_INFO, handleInfoRequest);
ipcMain.on(MODAL_RESULT, handleModalResult); ipcMain.on(MODAL_RESULT, handleModalResult);
ipcMain.on(MODAL_CANCEL, handleModalCancel); ipcMain.on(MODAL_CANCEL, handleModalCancel);
@ -118,3 +128,9 @@ ipcMain.on(EMIT_CONFIGURATION, (event: IpcMainEvent, config: CombinedConfig) =>
modal.view.webContents.send(DARK_MODE_CHANGE, config.darkMode); modal.view.webContents.send(DARK_MODE_CHANGE, config.darkMode);
}); });
}); });
function handleGetModalUncloseable(event: IpcMainInvokeEvent) {
const modalView = modalQueue.find((modal) => modal.view.webContents.id === event.sender.id);
return modalView?.uncloseable;
}

View file

@ -24,8 +24,9 @@ export class ModalView<T, T2> {
windowAttached?: BrowserWindow; windowAttached?: BrowserWindow;
status: Status; status: Status;
contextMenu: ContextMenu; contextMenu: ContextMenu;
uncloseable: boolean;
constructor(key: string, html: string, preload: string, data: T, onResolve: (value: T2) => void, onReject: (value: T2) => void, currentWindow: BrowserWindow) { constructor(key: string, html: string, preload: string, data: T, onResolve: (value: T2) => void, onReject: (value: T2) => void, currentWindow: BrowserWindow, uncloseable: boolean) {
this.key = key; this.key = key;
this.html = html; this.html = html;
this.data = data; this.data = data;
@ -42,6 +43,7 @@ export class ModalView<T, T2> {
this.onReject = onReject; this.onReject = onReject;
this.onResolve = onResolve; this.onResolve = onResolve;
this.window = currentWindow; this.window = currentWindow;
this.uncloseable = uncloseable;
this.status = Status.ACTIVE; this.status = Status.ACTIVE;
try { try {

View file

@ -10,7 +10,7 @@ import {TeamWithIndex} from 'types/config';
import urlUtils from 'common/utils/url'; import urlUtils from 'common/utils/url';
type Props = { type Props = {
onClose: () => void; onClose?: () => void;
onSave?: (team: TeamWithIndex) => void; onSave?: (team: TeamWithIndex) => void;
team?: TeamWithIndex; team?: TeamWithIndex;
editMode?: boolean; editMode?: boolean;
@ -175,7 +175,7 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
e.stopPropagation(); e.stopPropagation();
break; break;
case 'Escape': case 'Escape':
this.props.onClose(); this.props.onClose?.();
break; break;
} }
}} }}
@ -237,17 +237,25 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
{this.getError()} {this.getError()}
</div> </div>
<Button {this.props.onClose &&
id='cancelNewServerModal' <Button
onClick={this.props.onClose} id='cancelNewServerModal'
variant='link' onClick={this.props.onClose}
>{'Cancel'}</Button> variant='link'
<Button >
id='saveNewServerModal' {'Cancel'}
onClick={this.save} </Button>
disabled={!this.validateForm()} }
variant='primary' {this.props.onSave &&
>{this.getSaveButtonLabel()}</Button> <Button
id='saveNewServerModal'
onClick={this.save}
disabled={!this.validateForm()}
variant='primary'
>
{this.getSaveButtonLabel()}
</Button>
}
</Modal.Footer> </Modal.Footer>
</Modal> </Modal>

View file

@ -4,12 +4,13 @@
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/modals.css'; import 'renderer/css/modals.css';
import React from 'react'; import React, {useEffect, useState} from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import {TeamWithIndex} from 'types/config'; import {TeamWithIndex} from 'types/config';
import {ModalMessage} from 'types/modals';
import {MODAL_CANCEL, MODAL_RESULT} from 'common/communication'; import {GET_MODAL_UNCLOSEABLE, MODAL_CANCEL, MODAL_RESULT, MODAL_UNCLOSEABLE} from 'common/communication';
import NewTeamModal from '../../components/NewTeamModal'; //'./addServer.jsx'; import NewTeamModal from '../../components/NewTeamModal'; //'./addServer.jsx';
@ -25,14 +26,42 @@ const onSave = (data: TeamWithIndex) => {
window.postMessage({type: MODAL_RESULT, data}, window.location.href); window.postMessage({type: MODAL_RESULT, data}, window.location.href);
}; };
const start = async () => { const NewServerModalWrapper: React.FC = () => {
ReactDOM.render( const [unremoveable, setUnremovable] = useState<boolean>();
const handleNewServerMessage = (event: {data: ModalMessage<boolean>}) => {
switch (event.data.type) {
case MODAL_UNCLOSEABLE: {
setUnremovable(event.data.data);
break;
}
default:
break;
}
};
useEffect(() => {
window.addEventListener('message', handleNewServerMessage);
window.postMessage({type: GET_MODAL_UNCLOSEABLE}, window.location.href);
return () => {
window.removeEventListener('message', handleNewServerMessage);
};
}, []);
return (
<NewTeamModal <NewTeamModal
onClose={onClose} onClose={unremoveable ? undefined : onClose}
onSave={onSave} onSave={onSave}
editMode={false} editMode={false}
show={true} show={true}
/>, />
);
};
const start = async () => {
ReactDOM.render(
<NewServerModalWrapper/>,
document.getElementById('app'), document.getElementById('app'),
); );
}; };