feat: SDA-2775: Notification Position Window New UI (#1164)

* SDA-2775: notification position window new ui

- Create new user interface for notification position window
- Add unit tests

* SDA-2775: increase test coverage and format css
This commit is contained in:
Vishwas Shashidhar 2020-12-31 10:06:25 +05:30 committed by GitHub
parent 6fdb80857f
commit 8912c25003
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 562 additions and 125 deletions

View File

@ -0,0 +1,133 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Notification Settings should mount, unmount and render component should render the component 1`] = `
<div
className="content"
>
<header
className="header"
>
<span
className="header-title"
>
Set Notification Position
</span>
</header>
<div
className="form"
>
<label
className="display-label"
>
Show on display
</label>
<div
className="display-container"
id="screens"
>
<select
className="display-selector"
id="screen-selector"
onChange={[Function]}
title="position"
value={1}
/>
</div>
<label
className="position-label"
>
Position
</label>
<div
className="position-container"
>
<div
className="button-set-left"
>
<div
className="position-button-container"
>
<button
className="position-button upper-left"
id="upper-left"
name="position"
onClick={[Function]}
type="button"
value="upper-left"
>
Top Left
</button>
</div>
<div
className="position-button-container"
>
<button
className="position-button lower-left"
id="lower-left"
name="position"
onClick={[Function]}
type="button"
value="lower-left"
>
Bottom Left
</button>
</div>
</div>
<div
className="button-set-right"
>
<div
className="position-button-container"
>
<button
className="position-button position-button-selected upper-right"
id="upper-right"
name="position"
onClick={[Function]}
type="button"
value="upper-right"
>
Top Right
</button>
</div>
<div
className="position-button-container"
>
<button
className="position-button lower-right"
id="lower-right"
name="position"
onClick={[Function]}
type="button"
value="lower-right"
>
Bottom Right
</button>
</div>
</div>
</div>
</div>
<footer
className="footer"
>
<div
className="footer-button-container"
>
<button
className="footer-button footer-button-dismiss"
id="cancel"
onClick={[Function]}
>
CANCEL
</button>
<button
className="footer-button footer-button-ok"
id="ok-button"
onClick={[Function]}
>
OK
</button>
</div>
</footer>
</div>
`;

View File

@ -0,0 +1,224 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import NotificationSettings from '../src/renderer/components/notification-settings';
import { ipcRenderer } from './__mocks__/electron';
describe('Notification Settings', () => {
const notificationSettingsLabel = 'notification-settings-data';
const notificationSettingsMock = {
position: 'upper-right',
screens: [
{
id: '6713899',
},
{
id: '3512909',
},
],
display: '6713899',
};
const onLabelEvent = 'on';
const sendEvent = 'send';
const removeListenerLabelEvent = 'removeListener';
describe('should mount, unmount and render component', () => {
it('should render the component', () => {
const wrapper = shallow(React.createElement(NotificationSettings));
expect(wrapper).toMatchSnapshot();
});
it('should call `notification-settings-data` event when component is mounted', () => {
const spy = jest.spyOn(ipcRenderer, onLabelEvent);
shallow(React.createElement(NotificationSettings));
expect(spy).toBeCalledWith(notificationSettingsLabel, expect.any(Function));
});
it('should call `updateState` when component is mounted', () => {
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
expect(spy).toBeCalledWith(notificationSettingsMock);
});
it('should remove listener `notification-settings-data` when component is unmounted', () => {
const spyMount = jest.spyOn(ipcRenderer, onLabelEvent);
const spyUnmount = jest.spyOn(ipcRenderer, removeListenerLabelEvent);
const wrapper = shallow(React.createElement(NotificationSettings));
expect(spyMount).toBeCalledWith(notificationSettingsLabel, expect.any(Function));
wrapper.unmount();
expect(spyUnmount).toBeCalledWith(notificationSettingsLabel, expect.any(Function));
});
});
describe('should select display', () => {
it('should select display from drop down', () => {
const notificationSettingsMock = {
position: 'upper-right',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const selectDisplaySpy = jest.spyOn(NotificationSettings.prototype, 'selectDisplay');
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const positionButton = `select.display-selector`;
const input = wrapper.find(positionButton);
input.simulate('change', { target: { value: '6713899' } });
expect(selectDisplaySpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
});
describe('should set display position', () => {
it('should select top right position', () => {
const notificationSettingsMock = {
position: 'upper-right',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const togglePositionButtonSpy = jest.spyOn(NotificationSettings.prototype, 'togglePosition');
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const positionButton = `button.upper-right`;
const input = wrapper.find(positionButton);
input.simulate('click', { target: { value: 'upper-right' } });
expect(togglePositionButtonSpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
it('should select bottom right position', () => {
const notificationSettingsMock = {
position: 'bottom-right',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const togglePositionButtonSpy = jest.spyOn(NotificationSettings.prototype, 'togglePosition');
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const positionButton = `button.lower-right`;
const input = wrapper.find(positionButton);
input.simulate('click', { target: { value: 'lower-right' } });
expect(togglePositionButtonSpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
it('should select top left position', () => {
const notificationSettingsMock = {
position: 'upper-left',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const togglePositionButtonSpy = jest.spyOn(NotificationSettings.prototype, 'togglePosition');
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const positionButton = `button.upper-left`;
const input = wrapper.find(positionButton);
input.simulate('click', { target: { value: 'upper-left' } });
expect(togglePositionButtonSpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
it('should select bottom left position', () => {
const notificationSettingsMock = {
position: 'lower-left',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const togglePositionButtonSpy = jest.spyOn(NotificationSettings.prototype, 'togglePosition');
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const positionButton = `button.lower-left`;
const input = wrapper.find(positionButton);
input.simulate('click', { target: { value: 'lower-left' } });
expect(togglePositionButtonSpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
});
describe('should submit or cancel new preferences', () => {
it('should close window on pressing cancel button', () => {
const notificationSettingsMock = {
position: 'lower-left',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(ipcRenderer, sendEvent);
const closeButtonSpy = jest.spyOn(NotificationSettings.prototype, 'close');
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const closeButton = `button.footer-button-dismiss`;
const input = wrapper.find(closeButton);
input.simulate('click');
expect(closeButtonSpy).toBeCalled();
expect(spy).toBeCalledWith('symphony-api', {
cmd: 'close-window',
windowType: 'notification-settings',
});
});
it('should submit new preferences on pressing ok button', () => {
const notificationSettingsMock = {
position: 'lower-left',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(ipcRenderer, sendEvent);
const closeButtonSpy = jest.spyOn(NotificationSettings.prototype, 'close');
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const closeButton = `button.footer-button-ok`;
const input = wrapper.find(closeButton);
input.simulate('click');
expect(closeButtonSpy).toBeCalled();
expect(spy).toBeCalledWith('notification-settings-update', {
position: 'lower-left',
display: '6713899',
});
});
});
});

View File

@ -1336,8 +1336,8 @@ export class WindowHandler {
public createNotificationSettingsWindow(windowName: string): void {
const opts = this.getWindowOpts(
{
width: 460,
height: 360,
width: 540,
height: 440,
show: false,
modal: true,
minimizable: false,

View File

@ -107,7 +107,9 @@
"Position": "Position",
"Symphony - Configure Notification Position": "Symphony - Configure Notification Position",
"Top Left": "Top Left",
"Top Right": "Top Right"
"Top Right": "Top Right",
"Show on display": "Show on display",
"Set Notification Position": "Set Notification Position"
},
"Oops! Looks like we have had a crash.": "Oops! Looks like we have had a crash.",
"Oops! Looks like we have had a crash. Please reload or close this window.": "Oops! Looks like we have had a crash. Please reload or close this window.",

View File

@ -107,7 +107,9 @@
"Position": "Position",
"Symphony - Configure Notification Position": "Symphony - Configure Notification Position",
"Top Left": "Top Left",
"Top Right": "Top Right"
"Top Right": "Top Right",
"Show on display": "Show on display",
"Set Notification Position": "Set Notification Position"
},
"Oops! Looks like we have had a crash.": "Oops! Looks like we have had a crash.",
"Oops! Looks like we have had a crash. Please reload or close this window.": "Oops! Looks like we have had a crash. Please reload or close this window.",

View File

@ -107,7 +107,9 @@
"Position": "Position",
"Symphony - Configure Notification Position": "Symphony - Configurer la position des notifications",
"Top Left": "Gauche supérieure",
"Top Right": "Droite supérieure"
"Top Right": "Droite supérieure",
"Show on display": "Afficher sur écran",
"Set Notification Position": "Définir la position de notification"
},
"Oops! Looks like we have had a crash.": "Oops! On dirait que nous avons eu un crash.",
"Oops! Looks like we have had a crash. Please reload or close this window.": "Oops! On dirait que nous avons eu un crash. Veuillez recharger ou fermer cette fenêtre.",
@ -215,4 +217,4 @@
"Zoom": "Zoom",
"Zoom In": "Zoom Avant",
"Zoom Out": "Zoom Arrière"
}
}

View File

@ -107,7 +107,9 @@
"Position": "Position",
"Symphony - Configure Notification Position": "Symphony - Configurer la position des notifications",
"Top Left": "Gauche supérieure",
"Top Right": "Droite supérieure"
"Top Right": "Droite supérieure",
"Show on display": "Afficher sur écran",
"Set Notification Position": "Définir la position de notification"
},
"Oops! Looks like we have had a crash.": "Oops! On dirait que nous avons eu un crash.",
"Oops! Looks like we have had a crash. Please reload or close this window.": "Oops! On dirait que nous avons eu un crash. Veuillez recharger ou fermer cette fenêtre.",
@ -215,4 +217,4 @@
"Zoom": "Zoom",
"Zoom In": "Zoom Avant",
"Zoom Out": "Zoom Arrière"
}
}

View File

@ -107,7 +107,9 @@
"Position": "位置",
"Symphony - Configure Notification Position": "Symphony - 通知位置の設定",
"Top Left": "左上",
"Top Right": "右上"
"Top Right": "右上",
"Show on display": "ディスプレイに表示",
"Set Notification Position": "通知位置を設定する"
},
"Oops! Looks like we have had a crash.": "おっと!クラッシュしたようです。",
"Oops! Looks like we have had a crash. Please reload or close this window.": "おっと!クラッシュしたようです。このウィンドウを再度読み込むか閉じてください。",
@ -215,4 +217,4 @@
"Zoom": "ズーム",
"Zoom In": "ズームイン",
"Zoom Out": "ズームアウト"
}
}

View File

@ -107,7 +107,9 @@
"Position": "位置",
"Symphony - Configure Notification Position": "Symphony - 通知位置の設定",
"Top Left": "左上",
"Top Right": "右上"
"Top Right": "右上",
"Show on display": "ディスプレイに表示",
"Set Notification Position": "通知位置を設定する"
},
"Oops! Looks like we have had a crash.": "おっと!クラッシュしたようです。",
"Oops! Looks like we have had a crash. Please reload or close this window.": "おっと!クラッシュしたようです。このウィンドウを再度読み込むか閉じてください。",
@ -215,4 +217,4 @@
"Zoom": "ズーム",
"Zoom In": "ズームイン",
"Zoom Out": "ズームアウト"
}
}

View File

@ -15,17 +15,10 @@ interface IState {
}
/**
* Window that display app version and copyright info
* Notification Window component
*/
export default class NotificationSettings extends React.Component<{}, IState> {
private readonly eventHandlers = {
onTogglePosition: (e: React.ChangeEvent<HTMLInputElement>) => this.togglePosition(e),
onDisplaySelect: (e: React.ChangeEvent<HTMLSelectElement>) => this.selectDisplay(e),
onClose: () => this.close(),
onSubmit: () => this.submit(),
};
constructor(props) {
super(props);
this.state = {
@ -37,128 +30,98 @@ export default class NotificationSettings extends React.Component<{}, IState> {
}
/**
* main render function
* Renders the notification settings window
*/
public render(): JSX.Element {
return (
<div className='content'>
<header className='header'>
<span className='header__title'>
{i18n.t('Notification Settings', NOTIFICATION_SETTINGS_NAMESPACE)()}
<span className='header-title'>
{i18n.t('Set Notification Position', NOTIFICATION_SETTINGS_NAMESPACE)()}
</span>
</header>
<div className='form'>
<form>
<label className='label'>{i18n.t('Monitor', NOTIFICATION_SETTINGS_NAMESPACE)()}</label>
<div id='screens' className='main'>
<label>
{i18n.t('Notification shown on Monitor: ', NOTIFICATION_SETTINGS_NAMESPACE)()}
</label>
<select
className='selector'
id='screen-selector'
title='position'
value={this.state.display}
onChange={this.eventHandlers.onDisplaySelect}
>
{this.renderScreens()}
</select>
<label className='display-label'>{i18n.t('Show on display', NOTIFICATION_SETTINGS_NAMESPACE)()}</label>
<div id='screens' className='display-container'>
<select
className='display-selector'
id='screen-selector'
title='position'
value={this.state.display}
onChange={this.selectDisplay.bind(this)}
>
{this.renderScreens()}
</select>
</div>
<label className='position-label'>{i18n.t('Position', NOTIFICATION_SETTINGS_NAMESPACE)()}</label>
<div className='position-container'>
<div className='button-set-left'>
{this.renderPositionButton('upper-left', 'Top Left')}
{this.renderPositionButton('lower-left', 'Bottom Left')}
</div>
<label className='label'>{i18n.t('Position', NOTIFICATION_SETTINGS_NAMESPACE)()}</label>
<div className='main'>
<div className='first-set'>
{this.renderRadioButtons('upper-left', 'Top Left')}
{this.renderRadioButtons('lower-left', 'Bottom Left')}
</div>
<div className='second-set'>
{this.renderRadioButtons('upper-right', 'Top Right')}
{this.renderRadioButtons('lower-right', 'Bottom Right')}
</div>
<div className='button-set-right'>
{this.renderPositionButton('upper-right', 'Top Right')}
{this.renderPositionButton('lower-right', 'Bottom Right')}
</div>
</form>
</div>
</div>
<footer className='footer'>
<div className='buttonLayout'>
<button id='cancel' className='buttonDismiss' onClick={this.eventHandlers.onClose}>
<div className='footer-button-container'>
<button id='cancel' className='footer-button footer-button-dismiss' onClick={this.close.bind(this)}>
{i18n.t('CANCEL', NOTIFICATION_SETTINGS_NAMESPACE)()}
</button>
<button id='ok-button' className='button' onClick={this.eventHandlers.onSubmit}>
<button id='ok-button' className='footer-button footer-button-ok' onClick={this.submit.bind(this)}>
{i18n.t('OK', NOTIFICATION_SETTINGS_NAMESPACE)()}
</button>
</div>
</footer>
</div>
);
}
/**
* Handles event when the component is mounted
*/
public componentDidMount(): void {
ipcRenderer.on('notification-settings-data', this.updateState);
}
/**
* Handles event when the component is unmounted
*/
public componentWillUnmount(): void {
ipcRenderer.removeListener('notification-settings-data', this.updateState);
}
/**
* Renders all 4 different notification position options
*
* @param id
* @param content
*/
private renderRadioButtons(id: startCorner, content: string): JSX.Element {
return (
<div className='radio'>
<label className='radio__label' htmlFor={id}>
{i18n.t(`${content}`, NOTIFICATION_SETTINGS_NAMESPACE)()}
</label>
<input
onChange={this.eventHandlers.onTogglePosition}
className={id}
id={id}
type='radio'
name='position'
checked={this.state.position === id}
value={id} />
</div>
);
}
/**
* Renders the drop down list of available screen
*/
private renderScreens(): JSX.Element[] {
const { screens } = this.state;
return screens.map((screen, index) => {
return (
<option id={String(screen.id)} key={screen.id} value={screen.id}>{index + 1}</option>
);
});
}
/**
* Updates the selected display state
*
* @param event
*/
private selectDisplay(event): void {
public selectDisplay(event): void {
this.setState({ display: event.target.value });
}
/**
* Updated the selected notification position
* Updates the selected notification position
*
* @param event
*/
private togglePosition(event): void {
public togglePosition(event): void {
this.setState({
position: event.currentTarget.value,
position: event.target.id,
});
}
/**
* Sends the user selected notification settings options
* Submits the new settings to the main process
*/
private submit(): void {
public submit(): void {
const { position, display } = this.state;
ipcRenderer.send('notification-settings-update', { position, display });
}
@ -166,13 +129,49 @@ export default class NotificationSettings extends React.Component<{}, IState> {
/**
* Closes the notification settings window
*/
private close(): void {
public close(): void {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.closeWindow,
windowType: 'notification-settings',
});
}
/**
* Renders the position buttons
*
* @param id
* @param content
*/
private renderPositionButton(id: startCorner, content: string): JSX.Element {
const style = this.state.position === id ? `position-button position-button-selected ${id}` : `position-button ${id}`;
return (
<div className='position-button-container'>
<button
onClick={this.togglePosition.bind(this)}
className={style}
id={id} type='button'
name='position'
value={id}
>
{i18n.t(`${content}`, NOTIFICATION_SETTINGS_NAMESPACE)()}
</button>
</div>
);
}
/**
* Renders the drop down list of available screens
*/
private renderScreens(): JSX.Element[] {
const { screens } = this.state;
return screens.map((screen, index) => {
const screenId = screen.id;
return (
<option id={String(screenId)} key={screenId} value={screenId}>{index + 1}/{screens.length}</option>
);
});
}
/**
* Sets the component state
*

View File

@ -30,8 +30,8 @@ body {
padding: 16px;
}
.header__title {
font-weight: 500;
.header-title {
font-weight: 300;
font-style: normal;
margin: 0 0 0 4px;
font-size: 1.4rem;
@ -49,51 +49,104 @@ body {
margin: 0 auto;
}
.selector {
padding: 0 9px 0 16px;
cursor: pointer;
margin-left: 10px;
.display-label {
width: 304px;
height: 16px;
font-size: 12px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #525760;
}
.main {
.display-container {
margin-bottom: 20px;
margin-top: 5px;
}
.display-selector {
padding: 0 20px 0 30px;
cursor: pointer;
width: 304px;
height: 32px;
border: 2px solid #7C7F86;
border-radius: 2px;
background-color: #FFFFFF;
}
.position-label {
font-size: 12px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
width: 304px;
height: 16px;
color: #525760;
}
.position-container {
display: flex;
flex-direction: row;
margin-bottom: 20px;
height: 100%;
border: 1px solid #ccc !important;
padding: 10px;
.first-set {
width: 304px;
height: 100%;
left: 8px;
top: 102px;
border-radius: 4px;
margin-bottom: 20px;
margin-top: 5px;
background: #F8F8F9;
.button-set-left {
flex-grow: 1;
}
.second-set {
.button-set-right {
flex-grow: 1;
text-align: right;
}
}
.radio {
.position-button-container {
line-height: 1.7;
.upper-right, .lower-right {
float: right;
margin-top: 6px;
.position-button {
width: 116px;
height: 32px;
top: 1px;
margin: 20px 20px 20px 20px;
border: 2px solid #7C7F86;
border-radius: 4px;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 14px;
line-height: 20px;
align-items: center;
color: #17181B;
}
.upper-left, .lower-left {
.position-button-selected {
background-color: #008EFF;
}
.upper-left,
.lower-left {
float: left;
margin-top: 6px;
}
}
.radio__label {
cursor: pointer;
padding: 5px;
input {
cursor: pointer;
.upper-right,
.lower-right {
float: right;
}
}
.footer {
@ -103,12 +156,28 @@ body {
align-items: center;
}
.buttonLayout {
.footer-button-container {
margin-left: auto;
display: flex;
align-items: center;
}
.buttonDismiss {
margin-right: 10px;
.footer-button {
border: 2px solid #7C7F86;
border-radius: 4px;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 14px;
line-height: 20px;
align-items: center;
}
.footer-button-ok {
margin-right: 5px;
}
.footer-button-dismiss {
margin-right: 10px;
}
}