diff --git a/CHANGELOG.md b/CHANGELOG.md index 185ff60f..40e32ec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ from the final changelog of the release. Release date: TBD ### Improvements +- Added a new team button next to the team tabs #### All Platforms - Suppress white screen which is displayed for a moment on startup diff --git a/src/browser/components/MainPage.jsx b/src/browser/components/MainPage.jsx index cd58cad9..6695c2be 100644 --- a/src/browser/components/MainPage.jsx +++ b/src/browser/components/MainPage.jsx @@ -10,6 +10,8 @@ const MattermostView = require('./MattermostView.jsx'); const TabBar = require('./TabBar.jsx'); const HoveringURL = require('./HoveringURL.jsx'); +const NewTeamModal = require('./NewTeamModal.jsx'); + // Todo: Need to consider better way to apply styles const styles = { hoveringURL: { @@ -36,7 +38,8 @@ const MainPage = React.createClass({ propTypes: { disablewebsecurity: React.PropTypes.bool.isRequired, onUnreadCountChange: React.PropTypes.func.isRequired, - teams: React.PropTypes.array.isRequired + teams: React.PropTypes.array.isRequired, + onTeamConfigChange: React.PropTypes.func.isRequired }, getInitialState() { @@ -127,6 +130,10 @@ const MainPage = React.createClass({ mattermost.goForward(); } }); + + ipcRenderer.on('add-server', () => { + this.addServer(); + }); }, componentDidUpdate(prevProps, prevState) { if (prevState.key !== this.state.key) { // i.e. When tab has been changed @@ -237,6 +244,11 @@ const MainPage = React.createClass({ this.setState({targetURL}); } }, + addServer() { + this.setState({ + showNewTeamModal: true + }); + }, render() { var self = this; @@ -253,6 +265,7 @@ const MainPage = React.createClass({ mentionAtActiveCounts={this.state.mentionAtActiveCounts} activeKey={this.state.key} onSelect={this.handleSelect} + onAddServer={this.addServer} /> ); @@ -296,6 +309,25 @@ const MainPage = React.createClass({ authServerURL = `${tmpURL.protocol}//${tmpURL.host}`; authInfo = this.state.loginQueue[0].authInfo; } + var modal = ( + { + this.setState({ + showNewTeamModal: false + }); + }} + onSave={(newTeam) => { + this.props.teams.push(newTeam); + this.setState({ + showNewTeamModal: false, + key: this.props.teams.length - 1 + }); + this.render(); + this.props.onTeamConfigChange(this.props.teams); + }} + /> + ); return (
} +
+ { modal } +
); } diff --git a/src/browser/components/NewTeamModal.jsx b/src/browser/components/NewTeamModal.jsx new file mode 100644 index 00000000..36a852ab --- /dev/null +++ b/src/browser/components/NewTeamModal.jsx @@ -0,0 +1,209 @@ +const React = require('react'); +const {Modal, Button, FormGroup, FormControl, ControlLabel, HelpBlock} = require('react-bootstrap'); + +class NewTeamModal extends React.Component { + constructor() { + super(); + + this.wasShown = false; + this.state = { + teamName: '', + teamUrl: '', + saveStarted: false + }; + } + + componentWillMount() { + this.initializeOnShow(); + } + + initializeOnShow() { + this.state = { + teamName: this.props.team ? this.props.team.name : '', + teamUrl: this.props.team ? this.props.team.url : '', + teamIndex: this.props.team ? this.props.team.index : false, + saveStarted: false + }; + } + + getTeamNameValidationError() { + if (!this.state.saveStarted) { + return null; + } + return this.state.teamName.length > 0 ? null : 'Name is required.'; + } + + getTeamNameValidationState() { + return this.getTeamNameValidationError() === null ? null : 'error'; + } + + handleTeamNameChange(e) { + this.setState({ + teamName: e.target.value + }); + } + + getTeamUrlValidationError() { + if (!this.state.saveStarted) { + return null; + } + if (this.state.teamUrl.length === 0) { + return 'URL is required.'; + } + if (!(/^https?:\/\/.*/).test(this.state.teamUrl.trim())) { + return 'URL should start with http:// or https://.'; + } + return null; + } + + getTeamUrlValidationState() { + return this.getTeamUrlValidationError() === null ? null : 'error'; + } + + handleTeamUrlChange(e) { + this.setState({ + teamUrl: e.target.value + }); + } + + getError() { + return this.getTeamNameValidationError() || this.getTeamUrlValidationError(); + } + + validateForm() { + return this.getTeamNameValidationState() === null && + this.getTeamUrlValidationState() === null; + } + + save() { + this.setState({ + saveStarted: true + }, () => { + if (this.validateForm()) { + this.props.onSave({ + url: this.state.teamUrl, + name: this.state.teamName, + index: this.state.teamIndex + }); + } + }); + } + + getSaveButtonLabel() { + if (this.props.editMode) { + return 'Save'; + } + return 'Add'; + } + + getModalTitle() { + if (this.props.editMode) { + return 'Edit Server'; + } + return 'Add Server'; + } + + render() { + const noBottomSpaceing = { + 'padding-bottom': 0, + 'margin-bottom': 0 + }; + + if (this.wasShown !== this.props.show && this.props.show) { + this.initializeOnShow(); + } + this.wasShown = this.props.show; + + return ( + { + switch (e.key) { + case 'Enter': + this.save(); + + // The add button from behind this might still be focused + e.preventDefault(); + e.stopPropagation(); + break; + case 'Escape': + this.props.onClose(); + break; + } + }} + > + + {this.getModalTitle()} + + + +
+ + {'Server Display Name'} + + + {'The name of the server displayed on your desktop app tab bar.'} + + + {'Server URL'} + + + {'The URL of your Mattermost server. Must start with http:// or https://.'} + +
+
+ + +
+ {this.getError()} +
+ + + +
+ +
+ ); + } +} + +NewTeamModal.propTypes = { + onClose: React.PropTypes.func, + onSave: React.PropTypes.func, + team: React.PropTypes.object, + editMode: React.PropTypes.boolean, + show: React.PropTypes.boolean +}; + +module.exports = NewTeamModal; diff --git a/src/browser/components/SettingsPage.jsx b/src/browser/components/SettingsPage.jsx index ed615dd3..4e5e6bdd 100644 --- a/src/browser/components/SettingsPage.jsx +++ b/src/browser/components/SettingsPage.jsx @@ -46,6 +46,11 @@ const SettingsPage = React.createClass({ }); }); } + ipcRenderer.on('add-server', () => { + this.setState({ + showAddTeamForm: true + }); + }); }, handleTeamsChange(teams) { this.setState({ @@ -141,6 +146,11 @@ const SettingsPage = React.createClass({ showAddTeamForm: !this.state.showAddTeamForm }); }, + setShowTeamFormVisibility(val) { + this.setState({ + showAddTeamForm: val + }); + }, handleFlashWindow() { this.setState({ notifications: { @@ -153,6 +163,23 @@ const SettingsPage = React.createClass({ showUnreadBadge: !this.refs.showUnreadBadge.props.checked }); }, + + updateTeam(index, newData) { + var teams = this.state.teams; + teams[index] = newData; + this.setState({ + teams + }); + }, + + addServer(team) { + var teams = this.state.teams; + teams.push(team); + this.setState({ + teams + }); + }, + render() { var teamsRow = ( @@ -160,7 +187,11 @@ const SettingsPage = React.createClass({ @@ -354,6 +385,7 @@ const SettingsPage = React.createClass({

{'⊞ Add new server'} diff --git a/src/browser/components/TabBar.jsx b/src/browser/components/TabBar.jsx index 4282bd2e..ece3d951 100644 --- a/src/browser/components/TabBar.jsx +++ b/src/browser/components/TabBar.jsx @@ -1,5 +1,5 @@ const React = require('react'); -const {Nav, NavItem} = require('react-bootstrap'); +const {Nav, NavItem, Button} = require('react-bootstrap'); class TabBar extends React.Component { render() { @@ -75,16 +75,30 @@ class TabBar extends React.Component { onSelect={this.props.onSelect} > { tabs } + { this.renderAddTeamButton() } ); } + + renderAddTeamButton() { + return ( + + ); + } } TabBar.propTypes = { activeKey: React.PropTypes.number, id: React.PropTypes.string, onSelect: React.PropTypes.func, - teams: React.PropTypes.array + teams: React.PropTypes.array, + onAddServer: React.PropTypes.func }; module.exports = TabBar; diff --git a/src/browser/components/TeamList.jsx b/src/browser/components/TeamList.jsx index 99ff8dc4..ddad0bb7 100644 --- a/src/browser/components/TeamList.jsx +++ b/src/browser/components/TeamList.jsx @@ -1,19 +1,23 @@ const React = require('react'); const {ListGroup} = require('react-bootstrap'); const TeamListItem = require('./TeamListItem.jsx'); -const TeamListItemNew = require('./TeamListItemNew.jsx'); +const NewTeamModal = require('./NewTeamModal.jsx'); const RemoveServerModal = require('./RemoveServerModal.jsx'); const TeamList = React.createClass({ propTypes: { onTeamsChange: React.PropTypes.func, showAddTeamForm: React.PropTypes.bool, - teams: React.PropTypes.array + teams: React.PropTypes.array, + addServer: React.PropTypes.func, + updateTeam: React.PropTypes.func, + toggleAddTeamForm: React.PropTypes.func, + setAddTeamFormVisibility: React.PropTypes.func }, getInitialState() { return { - showTeamListItemNew: false, + showEditTeamForm: false, indexToRemoveServer: -1, team: { url: '', @@ -40,7 +44,7 @@ const TeamList = React.createClass({ } this.setState({ - showTeamListItemNew: false, + showEditTeamForm: false, team: { url: '', name: '', @@ -52,7 +56,7 @@ const TeamList = React.createClass({ }, handleTeamEditing(teamName, teamUrl, teamIndex) { this.setState({ - showTeamListItemNew: true, + showEditTeamForm: true, team: { url: teamUrl, name: teamName, @@ -92,19 +96,45 @@ const TeamList = React.createClass({ ); }); - var addTeamForm; - if (this.props.showAddTeamForm || this.state.showTeamListItemNew) { - addTeamForm = ( - ); - } else { - addTeamForm = ''; - } + var addServerForm = ( + { + this.setState({ + showEditTeamForm: false, + team: { + name: '', + url: '', + index: false + } + }); + this.props.setAddTeamFormVisibility(false); + }} + onSave={(newTeam) => { + var teamData = { + name: newTeam.name, + url: newTeam.url + }; + if (this.props.showAddTeamForm) { + this.props.addServer(teamData); + } else { + this.props.updateTeam(newTeam.index, teamData); + } + this.setState({ + showNewTeamModal: false, + showEditTeamForm: false, + team: { + name: '', + url: '', + index: false + } + }); + this.render(); + this.props.setAddTeamFormVisibility(false); + }} + team={this.state.team} + />); const removeServer = this.props.teams[this.state.indexToRemoveServer]; const removeServerModal = ( @@ -123,7 +153,7 @@ const TeamList = React.createClass({ return ( { teamNodes } - { addTeamForm } + { addServerForm } { removeServerModal} ); diff --git a/src/browser/config/AppConfig.js b/src/browser/config/AppConfig.js new file mode 100644 index 00000000..c235db5c --- /dev/null +++ b/src/browser/config/AppConfig.js @@ -0,0 +1,22 @@ +const settings = require('../../common/settings'); +const {remote} = require('electron'); + +class AppConfig { + constructor(file) { + this.fileName = file; + try { + this.data = settings.readFileSync(file); + } catch (e) { + this.data = { + teams: [] + }; + } + } + + set(key, value) { + this.data[key] = value; + settings.writeFileSync(this.fileName, this.data); + } +} + +module.exports = new AppConfig(remote.app.getPath('userData') + '/config.json'); diff --git a/src/browser/css/index.css b/src/browser/css/index.css index b0c13b5b..713f53f9 100644 --- a/src/browser/css/index.css +++ b/src/browser/css/index.css @@ -16,3 +16,25 @@ opacity: 0.01; transition: opacity 500ms ease-in-out; } + +.btn-tabButton { + margin-top: 3px; + color: #333; + background-color: #fff; + border-color: #ccc; +} + +.btn-tabButton:hover { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} + +.has-error .control-label, +.has-error .help-block { + color: #333; +} + +.modal-error { + color: #a94442; +} diff --git a/src/browser/index.jsx b/src/browser/index.jsx index aa236382..18a7476a 100644 --- a/src/browser/index.jsx +++ b/src/browser/index.jsx @@ -9,19 +9,12 @@ const ReactDOM = require('react-dom'); const {remote, ipcRenderer} = require('electron'); const MainPage = require('./components/MainPage.jsx'); -const settings = require('../common/settings'); +const AppConfig = require('./config/AppConfig.js'); const badge = require('./js/badge'); remote.getCurrentWindow().removeAllListeners('focus'); -var config; -try { - const configFile = remote.app.getPath('userData') + '/config.json'; - config = settings.readFileSync(configFile); -} catch (e) { - window.location = 'settings.html'; -} -if (config.teams.length === 0) { +if (AppConfig.data.teams.length === 0) { window.location = 'settings.html'; } @@ -40,7 +33,7 @@ function showUnreadBadgeWindows(unreadCount, mentionCount) { if (mentionCount > 0) { const dataURL = badge.createDataURL(mentionCount.toString()); sendBadge(dataURL, 'You have unread mentions (' + mentionCount + ')'); - } else if (unreadCount > 0 && config.showUnreadBadge) { + } else if (unreadCount > 0 && AppConfig.data.showUnreadBadge) { const dataURL = badge.createDataURL('•'); sendBadge(dataURL, 'You have unread channels (' + unreadCount + ')'); } else { @@ -51,7 +44,7 @@ function showUnreadBadgeWindows(unreadCount, mentionCount) { function showUnreadBadgeOSX(unreadCount, mentionCount) { if (mentionCount > 0) { remote.app.dock.setBadge(mentionCount.toString()); - } else if (unreadCount > 0 && config.showUnreadBadge) { + } else if (unreadCount > 0 && AppConfig.data.showUnreadBadge) { remote.app.dock.setBadge('•'); } else { remote.app.dock.setBadge(''); @@ -89,11 +82,16 @@ function showUnreadBadge(unreadCount, mentionCount) { } } +function teamConfigChange(teams) { + AppConfig.set('teams', teams); +} + ReactDOM.render( , document.getElementById('content') ); diff --git a/src/browser/settings.html b/src/browser/settings.html index abf4b3f8..20e2183f 100644 --- a/src/browser/settings.html +++ b/src/browser/settings.html @@ -5,6 +5,7 @@ Settings + diff --git a/src/main/menus/app.js b/src/main/menus/app.js index 95fbc10b..784bb090 100644 --- a/src/main/menus/app.js +++ b/src/main/menus/app.js @@ -27,6 +27,11 @@ function createTemplate(mainWindow, config) { click() { mainWindow.loadURL('file://' + __dirname + '/browser/settings.html'); } + }, { + label: 'Sign in to Another Server', + click() { + mainWindow.webContents.send('add-server'); + } }, separatorItem, { role: 'hide' }, { @@ -41,6 +46,11 @@ function createTemplate(mainWindow, config) { click() { mainWindow.loadURL('file://' + __dirname + '/browser/settings.html'); } + }, { + label: 'Sign in to Another Server', + click() { + mainWindow.webContents.send('add-server'); + } }, separatorItem, { role: 'quit', accelerator: 'CmdOrCtrl+Q', diff --git a/test/specs/browser/index_test.js b/test/specs/browser/index_test.js index 7ca155bc..5796bcaf 100644 --- a/test/specs/browser/index_test.js +++ b/test/specs/browser/index_test.js @@ -171,4 +171,11 @@ describe('browser/index.html', function desc() { browserWindow.getTitle().should.eventually.equal('Title 1'); }); }); + + it('should open the new server prompt after clicking the add button', () => { + // See settings_test for specs that cover the actual prompt + return this.app.client.waitUntilWindowLoaded(). + click('#tabBarAddNewTeam'). + isExisting('#newServerModal').should.eventually.be.true; + }); }); diff --git a/test/specs/browser/settings_test.js b/test/specs/browser/settings_test.js index a12903ea..f0b07406 100644 --- a/test/specs/browser/settings_test.js +++ b/test/specs/browser/settings_test.js @@ -245,4 +245,107 @@ describe('browser/settings.html', function desc() { isExisting(modalTitleSelector).should.eventually.false; }); }); + + describe('NewTeamModal', () => { + beforeEach(() => { + env.addClientCommands(this.app.client); + return this.app.client. + loadSettingsPage(). + click('#addNewServer'); + }); + + it('should open the new server modal', () => { + return this.app.client.isExisting('#newServerModal').should.eventually.equal(true); + }); + + it('should close the window after clicking cancel', () => { + return this.app.client. + click('#cancelNewServerModal'). + pause(1000). // Animation + isExisting('#newServerModal').should.eventually.equal(false); + }); + + it('should not be valid if no team name has been set', () => { + return this.app.client. + click('#saveNewServerModal'). + isExisting('.has-error #teamNameInput').should.eventually.equal(true); + }); + + it('should not be valid if no server address has been set', () => { + return this.app.client. + click('#saveNewServerModal'). + isExisting('.has-error #teamUrlInput').should.eventually.equal(true); + }); + + describe('Valid server name', () => { + beforeEach(() => { + return this.app.client. + setValue('#teamNameInput', 'TestTeam'). + click('#saveNewServerModal'); + }); + + it('should not be marked invalid', () => { + return this.app.client. + isExisting('.has-error #teamNameInput').should.eventually.equal(false); + }); + + it('should not be possible to click save', () => { + return this.app.client. + getAttribute('#saveNewServerModal', 'disabled').should.eventually.equal('true'); + }); + }); + + describe('Valid server url', () => { + beforeEach(() => { + return this.app.client. + setValue('#teamUrlInput', 'http://example.org'). + click('#saveNewServerModal'); + }); + + it('should be valid', () => { + return this.app.client. + isExisting('.has-error #teamUrlInput').should.eventually.equal(false); + }); + + it('should not be possible to click save', () => { + return this.app.client. + getAttribute('#saveNewServerModal', 'disabled').should.eventually.equal('true'); + }); + }); + + it('should not be valid if an invalid server address has been set', () => { + return this.app.client. + setValue('#teamUrlInput', 'superInvalid url'). + click('#saveNewServerModal'). + isExisting('.has-error #teamUrlInput').should.eventually.equal(true); + }); + + describe('Valid Team Settings', () => { + beforeEach(() => { + return this.app.client. + setValue('#teamUrlInput', 'http://example.org'). + setValue('#teamNameInput', 'TestTeam'); + }); + + it('should be possible to click add', () => { + return this.app.client. + getAttribute('#saveNewServerModal', 'disabled').should.eventually.equal(null); + }); + + it('should add the team to the config file', (done) => { + this.app.client. + click('#saveNewServerModal'). + pause(1000). // Animation + click('#btnSave'). + pause(1000).then(() => { + const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8')); + savedConfig.teams.should.contain({ + name: 'TestTeam', + url: 'http://example.org' + }); + return done(); + }); + }); + }); + }); });