diff --git a/src/browser/components/AutoSaveIndicator.jsx b/src/browser/components/AutoSaveIndicator.jsx
new file mode 100644
index 00000000..6685502e
--- /dev/null
+++ b/src/browser/components/AutoSaveIndicator.jsx
@@ -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 (
+
+ {message}
+
+ );
+}
+
+AutoSaveIndicator.propTypes = {
+ savingState: React.PropTypes.string.isRequired,
+ errorMessage: React.PropTypes.string
+};
+
+module.exports = AutoSaveIndicator;
diff --git a/src/browser/components/MainPage.jsx b/src/browser/components/MainPage.jsx
index 6695c2be..84efa920 100644
--- a/src/browser/components/MainPage.jsx
+++ b/src/browser/components/MainPage.jsx
@@ -39,12 +39,13 @@ const MainPage = React.createClass({
disablewebsecurity: React.PropTypes.bool.isRequired,
onUnreadCountChange: React.PropTypes.func.isRequired,
teams: React.PropTypes.array.isRequired,
- onTeamConfigChange: React.PropTypes.func.isRequired
+ onTeamConfigChange: React.PropTypes.func.isRequired,
+ initialIndex: React.PropTypes.number.isRequired
},
getInitialState() {
return {
- key: 0,
+ key: this.props.initialIndex,
unreadCounts: new Array(this.props.teams.length),
mentionCounts: new Array(this.props.teams.length),
unreadAtActive: new Array(this.props.teams.length),
diff --git a/src/browser/components/SettingsPage.jsx b/src/browser/components/SettingsPage.jsx
index 993b719f..02537904 100644
--- a/src/browser/components/SettingsPage.jsx
+++ b/src/browser/components/SettingsPage.jsx
@@ -4,18 +4,21 @@ const {Button, Checkbox, Col, FormGroup, Grid, HelpBlock, Navbar, Radio, Row} =
const {ipcRenderer, remote} = require('electron');
const AutoLaunch = require('auto-launch');
+const {debounce} = require('underscore');
const settings = require('../../common/settings');
const TeamList = require('./TeamList.jsx');
+const AutoSaveIndicator = require('./AutoSaveIndicator.jsx');
const appLauncher = new AutoLaunch({
name: 'Mattermost',
isHidden: true
});
-function backToIndex() {
- remote.getCurrentWindow().loadURL('file://' + __dirname + '/index.html');
+function backToIndex(index) {
+ const target = typeof index === 'undefined' ? 0 : index;
+ remote.getCurrentWindow().loadURL(`file://${__dirname}/index.html?index=${target}`);
}
const SettingsPage = React.createClass({
@@ -37,6 +40,7 @@ const SettingsPage = React.createClass({
if (initialState.teams.length === 0) {
initialState.showAddTeamForm = true;
}
+ initialState.savingState = 'done';
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) {
this.setState({
showAddTeamForm: false,
@@ -63,8 +95,10 @@ const SettingsPage = React.createClass({
if (teams.length === 0) {
this.setState({showAddTeamForm: true});
}
+ setImmediate(this.startSaveConfig);
},
- handleSave() {
+
+ saveConfig(callback) {
var config = {
teams: this.state.teams,
showTrayIcon: this.state.showTrayIcon,
@@ -77,23 +111,39 @@ const SettingsPage = React.createClass({
},
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-config');
-
- backToIndex();
+ settings.writeFile(this.props.configFile, config, (err) => {
+ 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() {
backToIndex();
},
@@ -101,6 +151,7 @@ const SettingsPage = React.createClass({
this.setState({
disablewebsecurity: this.refs.disablewebsecurity.props.checked
});
+ setImmediate(this.startSaveConfig);
},
handleChangeShowTrayIcon() {
var shouldShowTrayIcon = !this.refs.showTrayIcon.props.checked;
@@ -113,16 +164,20 @@ const SettingsPage = React.createClass({
minimizeToTray: false
});
}
+
+ setImmediate(this.startSaveConfig);
},
handleChangeTrayIconTheme() {
this.setState({
trayIconTheme: ReactDOM.findDOMNode(this.refs.trayIconTheme).value
});
+ setImmediate(this.startSaveConfig);
},
handleChangeAutoStart() {
this.setState({
autostart: !this.refs.autostart.props.checked
});
+ setImmediate(this.startSaveConfig);
},
handleChangeMinimizeToTray() {
const shouldMinimizeToTray = this.state.showTrayIcon && !this.refs.minimizeToTray.props.checked;
@@ -130,6 +185,7 @@ const SettingsPage = React.createClass({
this.setState({
minimizeToTray: shouldMinimizeToTray
});
+ setImmediate(this.startSaveConfig);
},
toggleShowTeamForm() {
this.setState({
@@ -147,11 +203,13 @@ const SettingsPage = React.createClass({
flashWindow: this.refs.flashWindow.props.checked ? 0 : 2
}
});
+ setImmediate(this.startSaveConfig);
},
handleShowUnreadBadge() {
this.setState({
showUnreadBadge: !this.refs.showUnreadBadge.props.checked
});
+ setImmediate(this.startSaveConfig);
},
updateTeam(index, newData) {
@@ -160,6 +218,7 @@ const SettingsPage = React.createClass({
this.setState({
teams
});
+ setImmediate(this.startSaveConfig);
},
addServer(team) {
@@ -168,6 +227,7 @@ const SettingsPage = React.createClass({
this.setState({
teams
});
+ setImmediate(this.startSaveConfig);
},
render() {
@@ -182,6 +242,7 @@ const SettingsPage = React.createClass({
onTeamsChange={this.handleTeamsChange}
updateTeam={this.updateTeam}
addServer={this.addServer}
+ onTeamClick={backToIndex}
/>
@@ -369,9 +430,16 @@ const SettingsPage = React.createClass({
className='navbar-fixed-top'
style={settingsPage.navbar}
>
+
{'Settings'}
);
}
diff --git a/src/browser/components/TeamList.jsx b/src/browser/components/TeamList.jsx
index ddad0bb7..195f3529 100644
--- a/src/browser/components/TeamList.jsx
+++ b/src/browser/components/TeamList.jsx
@@ -12,7 +12,8 @@ const TeamList = React.createClass({
addServer: React.PropTypes.func,
updateTeam: React.PropTypes.func,
toggleAddTeamForm: React.PropTypes.func,
- setAddTeamFormVisibility: React.PropTypes.func
+ setAddTeamFormVisibility: React.PropTypes.func,
+ onTeamClick: React.PropTypes.func
},
getInitialState() {
@@ -84,6 +85,10 @@ const TeamList = React.createClass({
self.handleTeamEditing(team.name, team.url, i);
}
+ function handleTeamClick() {
+ self.props.onTeamClick(i);
+ }
+
return (
);
});
diff --git a/src/browser/components/TeamListItem.jsx b/src/browser/components/TeamListItem.jsx
index 7a5223ae..8d128c13 100644
--- a/src/browser/components/TeamListItem.jsx
+++ b/src/browser/components/TeamListItem.jsx
@@ -16,12 +16,17 @@ class TeamListItem extends React.Component {
render() {
var style = {
left: {
- display: 'inline-block'
+ display: 'inline-block',
+ width: 'calc(100% - 100px)',
+ cursor: 'pointer'
}
};
return (
-
+
{ this.props.name }
{ this.props.url }
@@ -47,6 +52,7 @@ TeamListItem.propTypes = {
name: React.PropTypes.string,
onTeamEditing: React.PropTypes.func,
onTeamRemove: React.PropTypes.func,
+ onTeamClick: React.PropTypes.func,
url: React.PropTypes.string
};
diff --git a/src/browser/css/settings.css b/src/browser/css/settings.css
index c3c824b0..e64b4d54 100644
--- a/src/browser/css/settings.css
+++ b/src/browser/css/settings.css
@@ -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 {
width: 100%;
}
diff --git a/src/browser/index.jsx b/src/browser/index.jsx
index 18a7476a..ac1e0e24 100644
--- a/src/browser/index.jsx
+++ b/src/browser/index.jsx
@@ -10,6 +10,8 @@ const {remote, ipcRenderer} = require('electron');
const MainPage = require('./components/MainPage.jsx');
const AppConfig = require('./config/AppConfig.js');
+const url = require('url');
+
const badge = require('./js/badge');
remote.getCurrentWindow().removeAllListeners('focus');
@@ -86,10 +88,14 @@ function teamConfigChange(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(
,
diff --git a/src/common/settings.js b/src/common/settings.js
index cde9cc8c..e432c3ed 100644
--- a/src/common/settings.js
+++ b/src/common/settings.js
@@ -67,6 +67,14 @@ module.exports = {
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) {
if (config.version !== settingsVersion) {
throw new Error('version ' + config.version + ' is not equal to ' + settingsVersion);
diff --git a/src/package.json b/src/package.json
index 7ce051b6..226fb297 100644
--- a/src/package.json
+++ b/src/package.json
@@ -24,6 +24,7 @@
"react-addons-css-transition-group": "^15.4.1",
"react-bootstrap": "~0.30.7",
"react-dom": "^15.4.1",
+ "underscore": "^1.8.3",
"yargs": "^3.32.0"
}
}
diff --git a/test/specs/browser/settings_test.js b/test/specs/browser/settings_test.js
index 00641cce..19953ec6 100644
--- a/test/specs/browser/settings_test.js
+++ b/test/specs/browser/settings_test.js
@@ -31,22 +31,13 @@ describe('browser/settings.html', function desc() {
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);
return this.app.client.
loadSettingsPage().
- click('#btnCancel').
+ click('#btnClose').
pause(1000).
- 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$/);
+ getUrl().should.eventually.match(/\/index.html(\?.+)?$/);
});
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);
});
+ 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.skip('Hide Menu Bar', () => {
it('should appear on win32 or linux', () => {
@@ -87,7 +100,8 @@ describe('browser/settings.html', function desc() {
}
return true;
}).
- click('#btnSave').
+ pause(600).
+ click('#btnClose').
pause(1000).then(() => {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.hideMenuBar.should.equal(v);
@@ -120,7 +134,8 @@ describe('browser/settings.html', function desc() {
}
return true;
}).
- click('#btnSave').
+ pause(600).
+ click('#btnClose').
pause(1000).then(() => {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.disablewebsecurity.should.equal(!v);
@@ -226,7 +241,7 @@ describe('browser/settings.html', function desc() {
element('.modal-dialog').click('.btn=Remove').
pause(500).
isExisting(modalTitleSelector).should.eventually.false.
- click('#btnSave').
+ click('#btnClose').
pause(500).then(() => {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
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').
pause(500).
isExisting(modalTitleSelector).should.eventually.false.
- click('#btnSave').
+ click('#btnClose').
pause(500).then(() => {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.teams.should.deep.equal(config.teams);
@@ -352,7 +367,7 @@ describe('browser/settings.html', function desc() {
this.app.client.
click('#saveNewServerModal').
pause(1000). // Animation
- click('#btnSave').
+ click('#btnClose').
pause(1000).then(() => {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.teams.should.contain({