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({