Merge pull request #425 from yuya-oc/issue400-part2

Open the corresponding tab when a server is clicked in settings page
This commit is contained in:
Yuya Ochiai 2017-02-18 00:15:11 +09:00 committed by GitHub
commit f234f1bc95
10 changed files with 213 additions and 59 deletions

View file

@ -0,0 +1,41 @@
const React = require('react');
const {Alert} = require('react-bootstrap');
const baseClassName = 'AutoSaveIndicator';
const leaveClassName = `${baseClassName}-Leave`;
function getClassNameAndMessage(savingState, errorMessage) {
switch (savingState) {
case 'saving':
return {className: baseClassName, message: 'Saving...'};
case 'saved':
return {className: baseClassName, message: 'Saved'};
case 'error':
return {className: `${baseClassName}`, message: errorMessage};
case 'done':
return {className: `${baseClassName} ${leaveClassName}`, message: 'Saved'};
default:
return {className: `${baseClassName} ${leaveClassName}`, message: ''};
}
}
function AutoSaveIndicator(props) {
const {savingState, errorMessage, ...rest} = props;
const {className, message} = getClassNameAndMessage(savingState, errorMessage);
return (
<Alert
className={className}
{...rest}
bsStyle={savingState === 'error' ? 'danger' : 'info'}
>
{message}
</Alert>
);
}
AutoSaveIndicator.propTypes = {
savingState: React.PropTypes.string.isRequired,
errorMessage: React.PropTypes.string
};
module.exports = AutoSaveIndicator;

View file

@ -39,12 +39,13 @@ const MainPage = React.createClass({
disablewebsecurity: React.PropTypes.bool.isRequired, disablewebsecurity: React.PropTypes.bool.isRequired,
onUnreadCountChange: React.PropTypes.func.isRequired, onUnreadCountChange: React.PropTypes.func.isRequired,
teams: React.PropTypes.array.isRequired, teams: React.PropTypes.array.isRequired,
onTeamConfigChange: React.PropTypes.func.isRequired onTeamConfigChange: React.PropTypes.func.isRequired,
initialIndex: React.PropTypes.number.isRequired
}, },
getInitialState() { getInitialState() {
return { return {
key: 0, key: this.props.initialIndex,
unreadCounts: new Array(this.props.teams.length), unreadCounts: new Array(this.props.teams.length),
mentionCounts: new Array(this.props.teams.length), mentionCounts: new Array(this.props.teams.length),
unreadAtActive: new Array(this.props.teams.length), unreadAtActive: new Array(this.props.teams.length),

View file

@ -4,18 +4,21 @@ const {Button, Checkbox, Col, FormGroup, Grid, HelpBlock, Navbar, Radio, Row} =
const {ipcRenderer, remote} = require('electron'); const {ipcRenderer, remote} = require('electron');
const AutoLaunch = require('auto-launch'); const AutoLaunch = require('auto-launch');
const {debounce} = require('underscore');
const settings = require('../../common/settings'); const settings = require('../../common/settings');
const TeamList = require('./TeamList.jsx'); const TeamList = require('./TeamList.jsx');
const AutoSaveIndicator = require('./AutoSaveIndicator.jsx');
const appLauncher = new AutoLaunch({ const appLauncher = new AutoLaunch({
name: 'Mattermost', name: 'Mattermost',
isHidden: true isHidden: true
}); });
function backToIndex() { function backToIndex(index) {
remote.getCurrentWindow().loadURL('file://' + __dirname + '/index.html'); const target = typeof index === 'undefined' ? 0 : index;
remote.getCurrentWindow().loadURL(`file://${__dirname}/index.html?index=${target}`);
} }
const SettingsPage = React.createClass({ const SettingsPage = React.createClass({
@ -37,6 +40,7 @@ const SettingsPage = React.createClass({
if (initialState.teams.length === 0) { if (initialState.teams.length === 0) {
initialState.showAddTeamForm = true; initialState.showAddTeamForm = true;
} }
initialState.savingState = 'done';
return initialState; return initialState;
}, },
@ -55,6 +59,34 @@ const SettingsPage = React.createClass({
}); });
}); });
}, },
setSavingState(state) {
if (!this.setSavingStateSaved) {
this.setSavingStateSaved = debounce(() => {
this.saveConfig((err) => {
if (err) {
this.setState({savingState: 'error'});
} else {
this.setState({savingState: 'saved'});
}
this.setSavingStateDoneTimer = setTimeout(this.setState.bind(this, {savingState: 'done'}), 2000);
});
}, 500);
}
if (this.setSavingStateDoneTimer) {
clearTimeout(this.setSavingStateDoneTimer);
this.setSavingStateDoneTimer = null;
}
this.setState({savingState: state});
if (state === 'saving') {
this.setSavingStateSaved();
}
},
startSaveConfig() {
this.setSavingState('saving');
},
handleTeamsChange(teams) { handleTeamsChange(teams) {
this.setState({ this.setState({
showAddTeamForm: false, showAddTeamForm: false,
@ -63,8 +95,10 @@ const SettingsPage = React.createClass({
if (teams.length === 0) { if (teams.length === 0) {
this.setState({showAddTeamForm: true}); this.setState({showAddTeamForm: true});
} }
setImmediate(this.startSaveConfig);
}, },
handleSave() {
saveConfig(callback) {
var config = { var config = {
teams: this.state.teams, teams: this.state.teams,
showTrayIcon: this.state.showTrayIcon, showTrayIcon: this.state.showTrayIcon,
@ -77,23 +111,39 @@ const SettingsPage = React.createClass({
}, },
showUnreadBadge: this.state.showUnreadBadge showUnreadBadge: this.state.showUnreadBadge
}; };
settings.writeFileSync(this.props.configFile, config);
if (process.platform === 'win32' || process.platform === 'linux') {
var autostart = this.state.autostart;
appLauncher.isEnabled().then((enabled) => {
if (enabled && !autostart) {
appLauncher.disable();
} else if (!enabled && autostart) {
appLauncher.enable();
}
});
}
ipcRenderer.send('update-menu', config); ipcRenderer.send('update-menu', config);
ipcRenderer.send('update-config'); ipcRenderer.send('update-config');
settings.writeFile(this.props.configFile, config, (err) => {
backToIndex(); if (err) {
callback(err);
return;
}
if (process.platform === 'win32' || process.platform === 'linux') {
const autostart = this.state.autostart;
this.saveAutoStart(autostart, callback);
} else {
callback();
}
});
}, },
saveAutoStart(autostart, callback) {
appLauncher.isEnabled().then((enabled) => {
if (enabled && !autostart) {
appLauncher.disable().then(() => {
callback();
}).catch(callback);
} else if (!enabled && autostart) {
appLauncher.enable().then(() => {
callback();
}).catch(callback);
} else {
callback();
}
}).catch(callback);
},
handleCancel() { handleCancel() {
backToIndex(); backToIndex();
}, },
@ -101,6 +151,7 @@ const SettingsPage = React.createClass({
this.setState({ this.setState({
disablewebsecurity: this.refs.disablewebsecurity.props.checked disablewebsecurity: this.refs.disablewebsecurity.props.checked
}); });
setImmediate(this.startSaveConfig);
}, },
handleChangeShowTrayIcon() { handleChangeShowTrayIcon() {
var shouldShowTrayIcon = !this.refs.showTrayIcon.props.checked; var shouldShowTrayIcon = !this.refs.showTrayIcon.props.checked;
@ -113,16 +164,20 @@ const SettingsPage = React.createClass({
minimizeToTray: false minimizeToTray: false
}); });
} }
setImmediate(this.startSaveConfig);
}, },
handleChangeTrayIconTheme() { handleChangeTrayIconTheme() {
this.setState({ this.setState({
trayIconTheme: ReactDOM.findDOMNode(this.refs.trayIconTheme).value trayIconTheme: ReactDOM.findDOMNode(this.refs.trayIconTheme).value
}); });
setImmediate(this.startSaveConfig);
}, },
handleChangeAutoStart() { handleChangeAutoStart() {
this.setState({ this.setState({
autostart: !this.refs.autostart.props.checked autostart: !this.refs.autostart.props.checked
}); });
setImmediate(this.startSaveConfig);
}, },
handleChangeMinimizeToTray() { handleChangeMinimizeToTray() {
const shouldMinimizeToTray = this.state.showTrayIcon && !this.refs.minimizeToTray.props.checked; const shouldMinimizeToTray = this.state.showTrayIcon && !this.refs.minimizeToTray.props.checked;
@ -130,6 +185,7 @@ const SettingsPage = React.createClass({
this.setState({ this.setState({
minimizeToTray: shouldMinimizeToTray minimizeToTray: shouldMinimizeToTray
}); });
setImmediate(this.startSaveConfig);
}, },
toggleShowTeamForm() { toggleShowTeamForm() {
this.setState({ this.setState({
@ -147,11 +203,13 @@ const SettingsPage = React.createClass({
flashWindow: this.refs.flashWindow.props.checked ? 0 : 2 flashWindow: this.refs.flashWindow.props.checked ? 0 : 2
} }
}); });
setImmediate(this.startSaveConfig);
}, },
handleShowUnreadBadge() { handleShowUnreadBadge() {
this.setState({ this.setState({
showUnreadBadge: !this.refs.showUnreadBadge.props.checked showUnreadBadge: !this.refs.showUnreadBadge.props.checked
}); });
setImmediate(this.startSaveConfig);
}, },
updateTeam(index, newData) { updateTeam(index, newData) {
@ -160,6 +218,7 @@ const SettingsPage = React.createClass({
this.setState({ this.setState({
teams teams
}); });
setImmediate(this.startSaveConfig);
}, },
addServer(team) { addServer(team) {
@ -168,6 +227,7 @@ const SettingsPage = React.createClass({
this.setState({ this.setState({
teams teams
}); });
setImmediate(this.startSaveConfig);
}, },
render() { render() {
@ -182,6 +242,7 @@ const SettingsPage = React.createClass({
onTeamsChange={this.handleTeamsChange} onTeamsChange={this.handleTeamsChange}
updateTeam={this.updateTeam} updateTeam={this.updateTeam}
addServer={this.addServer} addServer={this.addServer}
onTeamClick={backToIndex}
/> />
</Col> </Col>
</Row> </Row>
@ -369,9 +430,16 @@ const SettingsPage = React.createClass({
className='navbar-fixed-top' className='navbar-fixed-top'
style={settingsPage.navbar} style={settingsPage.navbar}
> >
<div className='IndicatorContainer'>
<AutoSaveIndicator
savingState={this.state.savingState}
errorMessage={'Can\'t save your changes. Please try again.'}
/>
</div>
<div style={{position: 'relative'}}> <div style={{position: 'relative'}}>
<h1 style={settingsPage.heading}>{'Settings'}</h1> <h1 style={settingsPage.heading}>{'Settings'}</h1>
<Button <Button
id='btnClose'
bsStyle='link' bsStyle='link'
style={settingsPage.close} style={settingsPage.close}
onClick={this.handleCancel} onClick={this.handleCancel}
@ -383,7 +451,7 @@ const SettingsPage = React.createClass({
</Navbar> </Navbar>
<Grid <Grid
className='settingsPage' className='settingsPage'
style={{padding: '100px 15px'}} style={{paddingTop: '100px'}}
> >
<Row> <Row>
<Col <Col
@ -410,26 +478,6 @@ const SettingsPage = React.createClass({
<hr/> <hr/>
{ optionsRow } { optionsRow }
</Grid> </Grid>
<Navbar className='navbar-fixed-bottom'>
<div
className='text-right'
style={settingsPage.footer}
>
<Button
id='btnCancel'
className='btn-link'
onClick={this.handleCancel}
>{'Cancel'}</Button>
{ ' ' }
<Button
id='btnSave'
className='navbar-btn'
bsStyle='primary'
onClick={this.handleSave}
disabled={this.state.teams.length === 0}
>{'Save'}</Button>
</div>
</Navbar>
</div> </div>
); );
} }

View file

@ -12,7 +12,8 @@ const TeamList = React.createClass({
addServer: React.PropTypes.func, addServer: React.PropTypes.func,
updateTeam: React.PropTypes.func, updateTeam: React.PropTypes.func,
toggleAddTeamForm: React.PropTypes.func, toggleAddTeamForm: React.PropTypes.func,
setAddTeamFormVisibility: React.PropTypes.func setAddTeamFormVisibility: React.PropTypes.func,
onTeamClick: React.PropTypes.func
}, },
getInitialState() { getInitialState() {
@ -84,6 +85,10 @@ const TeamList = React.createClass({
self.handleTeamEditing(team.name, team.url, i); self.handleTeamEditing(team.name, team.url, i);
} }
function handleTeamClick() {
self.props.onTeamClick(i);
}
return ( return (
<TeamListItem <TeamListItem
index={i} index={i}
@ -92,6 +97,7 @@ const TeamList = React.createClass({
url={team.url} url={team.url}
onTeamRemove={handleTeamRemove} onTeamRemove={handleTeamRemove}
onTeamEditing={handleTeamEditing} onTeamEditing={handleTeamEditing}
onTeamClick={handleTeamClick}
/> />
); );
}); });

View file

@ -16,12 +16,17 @@ class TeamListItem extends React.Component {
render() { render() {
var style = { var style = {
left: { left: {
display: 'inline-block' display: 'inline-block',
width: 'calc(100% - 100px)',
cursor: 'pointer'
} }
}; };
return ( return (
<div className='teamListItem list-group-item'> <div className='teamListItem list-group-item'>
<div style={style.left}> <div
style={style.left}
onClick={this.props.onTeamClick}
>
<h4 className='list-group-item-heading'>{ this.props.name }</h4> <h4 className='list-group-item-heading'>{ this.props.name }</h4>
<p className='list-group-item-text'> <p className='list-group-item-text'>
{ this.props.url } { this.props.url }
@ -47,6 +52,7 @@ TeamListItem.propTypes = {
name: React.PropTypes.string, name: React.PropTypes.string,
onTeamEditing: React.PropTypes.func, onTeamEditing: React.PropTypes.func,
onTeamRemove: React.PropTypes.func, onTeamRemove: React.PropTypes.func,
onTeamClick: React.PropTypes.func,
url: React.PropTypes.string url: React.PropTypes.string
}; };

View file

@ -1,4 +1,26 @@
.teamListItem:hover {
background: #eee;
}
.IndicatorContainer {
position: absolute;
height: 100%;
display: flex;
flex-flow: row;
justify-content: flex-start;
align-items: center;
}
.AutoSaveIndicator {
margin: 0;
}
.AutoSaveIndicator.AutoSaveIndicator-Leave {
opacity: 0;
transition: opacity 1s cubic-bezier(0.19, 1, 0.22, 1);
}
.checkbox > label { .checkbox > label {
width: 100%; width: 100%;
} }

View file

@ -10,6 +10,8 @@ const {remote, ipcRenderer} = require('electron');
const MainPage = require('./components/MainPage.jsx'); const MainPage = require('./components/MainPage.jsx');
const AppConfig = require('./config/AppConfig.js'); const AppConfig = require('./config/AppConfig.js');
const url = require('url');
const badge = require('./js/badge'); const badge = require('./js/badge');
remote.getCurrentWindow().removeAllListeners('focus'); remote.getCurrentWindow().removeAllListeners('focus');
@ -86,10 +88,14 @@ function teamConfigChange(teams) {
AppConfig.set('teams', teams); AppConfig.set('teams', teams);
} }
const parsedURL = url.parse(window.location.href, true);
const initialIndex = parsedURL.query.index ? parseInt(parsedURL.query.index, 10) : 0;
ReactDOM.render( ReactDOM.render(
<MainPage <MainPage
disablewebsecurity={AppConfig.data.disablewebsecurity} disablewebsecurity={AppConfig.data.disablewebsecurity}
teams={AppConfig.data.teams} teams={AppConfig.data.teams}
initialIndex={initialIndex}
onUnreadCountChange={showUnreadBadge} onUnreadCountChange={showUnreadBadge}
onTeamConfigChange={teamConfigChange} onTeamConfigChange={teamConfigChange}
/>, />,

View file

@ -67,6 +67,14 @@ module.exports = {
return config; return config;
}, },
writeFile(configFile, config, callback) {
if (config.version !== settingsVersion) {
throw new Error('version ' + config.version + ' is not equal to ' + settingsVersion);
}
var data = JSON.stringify(config, null, ' ');
fs.writeFile(configFile, data, 'utf8', callback);
},
writeFileSync(configFile, config) { writeFileSync(configFile, config) {
if (config.version !== settingsVersion) { if (config.version !== settingsVersion) {
throw new Error('version ' + config.version + ' is not equal to ' + settingsVersion); throw new Error('version ' + config.version + ' is not equal to ' + settingsVersion);

View file

@ -24,6 +24,7 @@
"react-addons-css-transition-group": "^15.4.1", "react-addons-css-transition-group": "^15.4.1",
"react-bootstrap": "~0.30.7", "react-bootstrap": "~0.30.7",
"react-dom": "^15.4.1", "react-dom": "^15.4.1",
"underscore": "^1.8.3",
"yargs": "^3.32.0" "yargs": "^3.32.0"
} }
} }

View file

@ -31,22 +31,13 @@ describe('browser/settings.html', function desc() {
return true; return true;
}); });
it('should show index.html when Cancel button is clicked', () => { it('should show index.html when Close button is clicked', () => {
env.addClientCommands(this.app.client); env.addClientCommands(this.app.client);
return this.app.client. return this.app.client.
loadSettingsPage(). loadSettingsPage().
click('#btnCancel'). click('#btnClose').
pause(1000). pause(1000).
getUrl().should.eventually.match(/\/index.html$/); getUrl().should.eventually.match(/\/index.html(\?.+)?$/);
});
it('should show index.html when Save button is clicked', () => {
env.addClientCommands(this.app.client);
return this.app.client.
loadSettingsPage().
click('#btnSave').
pause(1000).
getUrl().should.eventually.match(/\/index.html$/);
}); });
it('should show NewServerModal after all servers are removed', () => { it('should show NewServerModal after all servers are removed', () => {
@ -65,6 +56,28 @@ describe('browser/settings.html', function desc() {
isExisting('#newServerModal').should.eventually.equal(true); isExisting('#newServerModal').should.eventually.equal(true);
}); });
describe('Server list', () => {
it('should open the corresponding tab when a server list item is clicked', () => {
env.addClientCommands(this.app.client);
return this.app.client.
loadSettingsPage().
click('h4=example_1').
pause(100).
waitUntilWindowLoaded().
getUrl().should.eventually.match(/\/index.html(\?.+)?$/).
isVisible('#mattermostView0').should.eventually.be.true.
isVisible('#mattermostView1').should.eventually.be.false.
loadSettingsPage().
click('h4=example_2').
pause(100).
waitUntilWindowLoaded().
getUrl().should.eventually.match(/\/index.html(\?.+)?$/).
isVisible('#mattermostView0').should.eventually.be.false.
isVisible('#mattermostView1').should.eventually.be.true;
});
});
describe('Options', () => { describe('Options', () => {
describe.skip('Hide Menu Bar', () => { describe.skip('Hide Menu Bar', () => {
it('should appear on win32 or linux', () => { it('should appear on win32 or linux', () => {
@ -87,7 +100,8 @@ describe('browser/settings.html', function desc() {
} }
return true; return true;
}). }).
click('#btnSave'). pause(600).
click('#btnClose').
pause(1000).then(() => { pause(1000).then(() => {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8')); const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.hideMenuBar.should.equal(v); savedConfig.hideMenuBar.should.equal(v);
@ -120,7 +134,8 @@ describe('browser/settings.html', function desc() {
} }
return true; return true;
}). }).
click('#btnSave'). pause(600).
click('#btnClose').
pause(1000).then(() => { pause(1000).then(() => {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8')); const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.disablewebsecurity.should.equal(!v); savedConfig.disablewebsecurity.should.equal(!v);
@ -226,7 +241,7 @@ describe('browser/settings.html', function desc() {
element('.modal-dialog').click('.btn=Remove'). element('.modal-dialog').click('.btn=Remove').
pause(500). pause(500).
isExisting(modalTitleSelector).should.eventually.false. isExisting(modalTitleSelector).should.eventually.false.
click('#btnSave'). click('#btnClose').
pause(500).then(() => { pause(500).then(() => {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8')); const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.teams.should.deep.equal(config.teams.slice(1)); savedConfig.teams.should.deep.equal(config.teams.slice(1));
@ -239,7 +254,7 @@ describe('browser/settings.html', function desc() {
element('.modal-dialog').click('.btn=Cancel'). element('.modal-dialog').click('.btn=Cancel').
pause(500). pause(500).
isExisting(modalTitleSelector).should.eventually.false. isExisting(modalTitleSelector).should.eventually.false.
click('#btnSave'). click('#btnClose').
pause(500).then(() => { pause(500).then(() => {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8')); const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.teams.should.deep.equal(config.teams); savedConfig.teams.should.deep.equal(config.teams);
@ -352,7 +367,7 @@ describe('browser/settings.html', function desc() {
this.app.client. this.app.client.
click('#saveNewServerModal'). click('#saveNewServerModal').
pause(1000). // Animation pause(1000). // Animation
click('#btnSave'). click('#btnClose').
pause(1000).then(() => { pause(1000).then(() => {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8')); const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.teams.should.contain({ savedConfig.teams.should.contain({