diff --git a/.eslintrc-webapp.json b/.eslintrc-webapp.json index c4c49643..0659e9ce 100644 --- a/.eslintrc-webapp.json +++ b/.eslintrc-webapp.json @@ -53,7 +53,7 @@ "react/jsx-filename-extension": [ 1, { - "extensions": [".js"] + "extensions": [".js", ".jsx"] } ], "react/prop-types": [ diff --git a/.eslintrc.json b/.eslintrc.json index bd9b3e25..19ca109b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -30,7 +30,6 @@ { "files": [ "webpack.config.renderer.js", - "test/specs/settings_test.js", "test/specs/spellchecker_test.js", "test/specs/app_test.js", "test/specs/security_test.js", @@ -51,7 +50,6 @@ "LICENSE.txt", "src/utils/util.js", "src/main.js", - "src/browser/config/AppConfig.js", "src/browser/js/contextMenu.js", "src/browser/updater.jsx", "src/browser/js/notification.js", @@ -78,13 +76,13 @@ "src/browser/settings.jsx", "src/browser/index.jsx", "src/common/deepmerge.js", + "src/common/config/index.js", "src/common/config/buildConfig.js", "src/common/config/pastDefaultPreferences.js", "src/common/config/upgradePreferences.js", "src/common/osVersion.js", "src/common/config/defaultPreferences.js", "src/common/JsonFileManager.js", - "src/common/settings.js", "src/main/certificateStore.js", "src/main/mainWindow.js", "src/main/allowProtocolDialog.js", diff --git a/src/browser/components/SettingsPage.jsx b/src/browser/components/SettingsPage.jsx index 81eabf11..27742fa8 100644 --- a/src/browser/components/SettingsPage.jsx +++ b/src/browser/components/SettingsPage.jsx @@ -6,120 +6,141 @@ /* eslint-disable react/no-set-state */ import React from 'react'; -import PropTypes from 'prop-types'; import {Button, Checkbox, Col, FormGroup, Grid, HelpBlock, Navbar, Radio, Row} from 'react-bootstrap'; import {ipcRenderer, remote} from 'electron'; import {debounce} from 'underscore'; -import buildConfig from '../../common/config/buildConfig'; -import settings from '../../common/settings'; +import Config from '../../common/config'; import TeamList from './TeamList.jsx'; import AutoSaveIndicator from './AutoSaveIndicator.jsx'; +const CONFIG_TYPE_SERVERS = 'servers'; +const CONFIG_TYPE_APP_OPTIONS = 'appOptions'; + +const config = new Config(remote.app.getPath('userData') + '/config.json'); + function backToIndex(index) { const target = typeof index === 'undefined' ? 0 : index; const indexURL = remote.getGlobal('isDev') ? 'http://localhost:8080/browser/index.html' : `file://${remote.app.getAppPath()}/browser/index.html`; remote.getCurrentWindow().loadURL(`${indexURL}?index=${target}`); } -const CONFIG_TYPE_SERVERS = 'servers'; -const CONFIG_TYPE_APP_OPTIONS = 'appOptions'; - export default class SettingsPage extends React.Component { constructor(props) { super(props); - let initialState; - try { - initialState = settings.readFileSync(this.props.configFile); - } catch (e) { - initialState = settings.loadDefault(); - } - initialState.showAddTeamForm = false; - initialState.trayWasVisible = remote.getCurrentWindow().trayWasVisible; - if (initialState.teams.length === 0) { - initialState.showAddTeamForm = true; - } - initialState.savingState = { - appOptions: AutoSaveIndicator.SAVING_STATE_DONE, - servers: AutoSaveIndicator.SAVING_STATE_DONE, - }; - this.state = initialState; - - this.startSaveConfig = this.startSaveConfig.bind(this); - this.didSaveConfig = this.didSaveConfig.bind(this); - this.handleTeamsChange = this.handleTeamsChange.bind(this); - this.saveConfig = this.saveConfig.bind(this); - this.handleCancel = this.handleCancel.bind(this); - this.handleChangeShowTrayIcon = this.handleChangeShowTrayIcon.bind(this); - this.handleChangeTrayIconTheme = this.handleChangeTrayIconTheme.bind(this); - this.handleChangeAutoStart = this.handleChangeAutoStart.bind(this); - this.handleChangeMinimizeToTray = this.handleChangeMinimizeToTray.bind(this); - this.toggleShowTeamForm = this.toggleShowTeamForm.bind(this); - this.setShowTeamFormVisibility = this.setShowTeamFormVisibility.bind(this); - this.handleFlashWindow = this.handleFlashWindow.bind(this); - this.handleBounceIcon = this.handleBounceIcon.bind(this); - this.handleBounceIconType = this.handleBounceIconType.bind(this); - this.handleShowUnreadBadge = this.handleShowUnreadBadge.bind(this); - this.handleChangeUseSpellChecker = this.handleChangeUseSpellChecker.bind(this); - this.handleChangeEnableHardwareAcceleration = this.handleChangeEnableHardwareAcceleration.bind(this); - this.updateTeam = this.updateTeam.bind(this); - this.addServer = this.addServer.bind(this); + this.state = this.convertConfigDataToState(config.data); this.trayIconThemeRef = React.createRef(); + + this.saveQueue = []; } componentDidMount() { + config.on('update', (configData) => { + this.updateSaveState(); + this.setState(this.convertConfigDataToState(configData, this.state)); + }); + + config.on('error', (error) => { + console.log('Config saving error: ', error); + + const savingState = Object.assign({}, this.state.savingState); + Object.entries(savingState).forEach(([configType, currentState]) => { + if (currentState !== AutoSaveIndicator.SAVING_STATE_DONE) { + savingState[configType] = AutoSaveIndicator.SAVING_STATE_ERROR; + this.setState({savingState}); + } + }); + }); + + // when the config object changes here in the renderer process, tell the main process to reload its config object to get the changes + config.on('synchronize', () => { + ipcRenderer.send('reload-config'); + }); + + // listen for any config reload requests from the main process to reload configuration changes here in the renderer process + ipcRenderer.on('reload-config', () => { + config.reload(); + }); + ipcRenderer.on('add-server', () => { this.setState({ showAddTeamForm: true, }); }); + ipcRenderer.on('switch-tab', (event, key) => { backToIndex(key); }); } - startSaveConfig(configType) { - if (!this.startSaveConfigImpl) { - this.startSaveConfigImpl = {}; - } - if (!this.startSaveConfigImpl[configType]) { - this.startSaveConfigImpl[configType] = debounce(() => { - this.saveConfig((err) => { - if (err) { - console.error(err); - } - const savingState = Object.assign({}, this.state.savingState); - savingState[configType] = err ? AutoSaveIndicator.SAVING_STATE_ERROR : AutoSaveIndicator.SAVING_STATE_SAVED; - this.setState({savingState}); - this.didSaveConfig(configType); - }); - }, 500); + convertConfigDataToState = (configData, currentState = {}) => { + const newState = Object.assign({}, configData); + newState.showAddTeamForm = currentState.showAddTeamForm || false; + newState.trayWasVisible = currentState.trayWasVisible || remote.getCurrentWindow().trayWasVisible; + if (newState.teams.length === 0 && currentState.firstRun !== false) { + newState.firstRun = false; + newState.showAddTeamForm = true; } + newState.savingState = currentState.savingState || { + appOptions: AutoSaveIndicator.SAVING_STATE_DONE, + servers: AutoSaveIndicator.SAVING_STATE_DONE, + }; + return newState; + } + + saveSetting = (configType, {key, data}) => { + this.saveQueue.push({ + configType, + key, + data, + }); + this.updateSaveState(); + this.processSaveQueue(); + } + + processSaveQueue = debounce(() => { + config.setMultiple(this.saveQueue.splice(0, this.saveQueue.length)); + }, 500); + + updateSaveState = () => { + let queuedUpdateCounts = { + [CONFIG_TYPE_SERVERS]: 0, + [CONFIG_TYPE_APP_OPTIONS]: 0, + }; + + queuedUpdateCounts = this.saveQueue.reduce((updateCounts, {configType}) => { + updateCounts[configType]++; + return updateCounts; + }, queuedUpdateCounts); + const savingState = Object.assign({}, this.state.savingState); - savingState[configType] = AutoSaveIndicator.SAVING_STATE_SAVING; + + Object.entries(queuedUpdateCounts).forEach(([configType, count]) => { + if (count > 0) { + savingState[configType] = AutoSaveIndicator.SAVING_STATE_SAVING; + } else if (count === 0 && savingState[configType] === AutoSaveIndicator.SAVING_STATE_SAVING) { + savingState[configType] = AutoSaveIndicator.SAVING_STATE_SAVED; + this.resetSaveState(configType); + } + }); + this.setState({savingState}); - this.startSaveConfigImpl[configType](); } - didSaveConfig(configType) { - if (!this.didSaveConfigImpl) { - this.didSaveConfigImpl = {}; + resetSaveState = debounce((configType) => { + if (this.state.savingState[configType] !== AutoSaveIndicator.SAVING_STATE_SAVING) { + const savingState = Object.assign({}, this.state.savingState); + savingState[configType] = AutoSaveIndicator.SAVING_STATE_DONE; + this.setState({savingState}); } - if (!this.didSaveConfigImpl[configType]) { - this.didSaveConfigImpl[configType] = debounce(() => { - const savingState = Object.assign({}, this.state.savingState); - savingState[configType] = AutoSaveIndicator.SAVING_STATE_DONE; - this.setState({savingState}); - }, 2000); - } - this.didSaveConfigImpl[configType](); - } + }, 2000); - handleTeamsChange(teams) { + handleTeamsChange = (teams) => { + setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams}); this.setState({ showAddTeamForm: false, teams, @@ -127,45 +148,15 @@ export default class SettingsPage extends React.Component { if (teams.length === 0) { this.setState({showAddTeamForm: true}); } - setImmediate(this.startSaveConfig, CONFIG_TYPE_SERVERS); } - saveConfig(callback) { - const config = { - teams: this.state.teams, - showTrayIcon: this.state.showTrayIcon, - trayIconTheme: this.state.trayIconTheme, - version: settings.version, - minimizeToTray: this.state.minimizeToTray, - notifications: { - flashWindow: this.state.notifications.flashWindow, - bounceIcon: this.state.notifications.bounceIcon, - bounceIconType: this.state.notifications.bounceIconType, - }, - showUnreadBadge: this.state.showUnreadBadge, - useSpellChecker: this.state.useSpellChecker, - spellCheckerLocale: this.state.spellCheckerLocale, - enableHardwareAcceleration: this.state.enableHardwareAcceleration, - autostart: this.state.autostart, - }; - - settings.writeFile(this.props.configFile, config, (err) => { - if (err) { - callback(err); - return; - } - ipcRenderer.send('update-menu', config); - ipcRenderer.send('update-config'); - callback(); - }); - } - - handleCancel() { + handleCancel = () => { backToIndex(); } - handleChangeShowTrayIcon() { + handleChangeShowTrayIcon = () => { const shouldShowTrayIcon = !this.refs.showTrayIcon.props.checked; + setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showTrayIcon', data: shouldShowTrayIcon}); this.setState({ showTrayIcon: shouldShowTrayIcon, }); @@ -175,113 +166,129 @@ export default class SettingsPage extends React.Component { minimizeToTray: false, }); } - - setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); } - handleChangeTrayIconTheme() { + handleChangeTrayIconTheme = (theme) => { + setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'trayIconTheme', data: theme}); this.setState({ - trayIconTheme: this.trayIconThemeRef.current.value, + trayIconTheme: theme, }); - setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); } - handleChangeAutoStart() { + handleChangeAutoStart = () => { + setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'autostart', data: !this.refs.autostart.props.checked}); this.setState({ autostart: !this.refs.autostart.props.checked, }); - setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); } - handleChangeMinimizeToTray() { + handleChangeMinimizeToTray = () => { const shouldMinimizeToTray = this.state.showTrayIcon && !this.refs.minimizeToTray.props.checked; + setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'minimizeToTray', data: shouldMinimizeToTray}); this.setState({ minimizeToTray: shouldMinimizeToTray, }); - setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); } - toggleShowTeamForm() { + toggleShowTeamForm = () => { this.setState({ showAddTeamForm: !this.state.showAddTeamForm, }); document.activeElement.blur(); } - setShowTeamFormVisibility(val) { + setShowTeamFormVisibility = (val) => { this.setState({ showAddTeamForm: val, }); } - handleFlashWindow() { + handleFlashWindow = () => { + setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, { + key: 'notifications', + data: { + ...this.state.notifications, + flashWindow: this.refs.flashWindow.props.checked ? 0 : 2, + }, + }); this.setState({ notifications: { ...this.state.notifications, flashWindow: this.refs.flashWindow.props.checked ? 0 : 2, }, }); - setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); } - handleBounceIcon() { + handleBounceIcon = () => { + setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, { + key: 'notifications', + data: { + ...this.state.notifications, + bounceIcon: !this.refs.bounceIcon.props.checked, + }, + }); this.setState({ notifications: { ...this.state.notifications, bounceIcon: !this.refs.bounceIcon.props.checked, }, }); - setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); } - handleBounceIconType(event) { + handleBounceIconType = (event) => { + setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, { + key: 'notifications', + data: { + ...this.state.notifications, + bounceIconType: event.target.value, + }, + }); this.setState({ notifications: { ...this.state.notifications, bounceIconType: event.target.value, }, }); - setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); } - handleShowUnreadBadge() { + handleShowUnreadBadge = () => { + setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showUnreadBadge', data: !this.refs.showUnreadBadge.props.checked}); this.setState({ showUnreadBadge: !this.refs.showUnreadBadge.props.checked, }); - setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); } - handleChangeUseSpellChecker() { + handleChangeUseSpellChecker = () => { + setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'useSpellChecker', data: !this.refs.useSpellChecker.props.checked}); this.setState({ useSpellChecker: !this.refs.useSpellChecker.props.checked, }); - setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); } - handleChangeEnableHardwareAcceleration() { + handleChangeEnableHardwareAcceleration = () => { + setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'enableHardwareAcceleration', data: !this.refs.enableHardwareAcceleration.props.checked}); this.setState({ enableHardwareAcceleration: !this.refs.enableHardwareAcceleration.props.checked, }); - setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); } - updateTeam(index, newData) { - const teams = this.state.teams; + updateTeam = (index, newData) => { + const teams = this.state.localTeams; teams[index] = newData; + setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams}); this.setState({ teams, }); - setImmediate(this.startSaveConfig, CONFIG_TYPE_SERVERS); } - addServer(team) { - const teams = this.state.teams; + addServer = (team) => { + const teams = this.state.localTeams; teams.push(team); + setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams}); this.setState({ teams, }); - setImmediate(this.startSaveConfig, CONFIG_TYPE_SERVERS); } render() { @@ -324,7 +331,7 @@ export default class SettingsPage extends React.Component { { - backToIndex(index + buildConfig.defaultTeams.length); + backToIndex(index + this.state.buildTeams.length + this.state.GPOTeams.length); }} /> @@ -372,7 +379,7 @@ export default class SettingsPage extends React.Component { ); let srvMgmt; - if (this.props.enableServerManagement === true) { + if (this.state.enableServerManagement === true) { srvMgmt = (
{serversRow} @@ -393,7 +400,8 @@ export default class SettingsPage extends React.Component { ref='autostart' checked={this.state.autostart} onChange={this.handleChangeAutoStart} - >{'Start app on login'} + > + {'Start app on login'} {'If enabled, the app starts automatically when you log in to your machine.'} {' '} @@ -426,7 +434,8 @@ export default class SettingsPage extends React.Component { ref='showUnreadBadge' checked={this.state.showUnreadBadge} onChange={this.handleShowUnreadBadge} - >{`Show red badge on ${TASKBAR} icon to indicate unread messages`} + > + {`Show red badge on ${TASKBAR} icon to indicate unread messages`} {`Regardless of this setting, mentions are always indicated with a red badge and item count on the ${TASKBAR} icon.`} @@ -441,7 +450,8 @@ export default class SettingsPage extends React.Component { ref='flashWindow' checked={this.state.notifications.flashWindow === 2} onChange={this.handleFlashWindow} - >{'Flash app window and taskbar icon when a new message is received'} + > + {'Flash app window and taskbar icon when a new message is received'} {'If enabled, app window and taskbar icon flash for a few seconds when a new message is received.'} @@ -459,7 +469,8 @@ export default class SettingsPage extends React.Component { checked={this.state.notifications.bounceIcon} onChange={this.handleBounceIcon} style={{marginRight: '10px'}} - >{'Bounce the Dock icon'} + > + {'Bounce the Dock icon'} {'once'} + > + {'once'} + {' '} {'until I open the app'} + > + {'until I open the app'} + @@ -498,14 +513,14 @@ export default class SettingsPage extends React.Component { ref='showTrayIcon' checked={this.state.showTrayIcon} onChange={this.handleChangeShowTrayIcon} - >{process.platform === 'darwin' ? - `Show ${remote.app.getName()} icon in the menu bar` : - 'Show icon in the notification area'} + > + {process.platform === 'darwin' ? `Show ${remote.app.getName()} icon in the menu bar` : 'Show icon in the notification area'} {'Setting takes effect after restarting the app.'} ); } + if (process.platform === 'linux') { options.push( { - this.setState({trayIconTheme: 'light'}); - setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); - }} - >{'Light'} + onChange={(event) => this.handleChangeTrayIconTheme('light', event)} + > + {'Light'} + {' '} { - this.setState({trayIconTheme: 'dark'}); - setImmediate(this.startSaveConfig, CONFIG_TYPE_APP_OPTIONS); - }} + onChange={(event) => this.handleChangeTrayIconTheme('dark', event)} >{'Dark'} ); @@ -607,7 +618,7 @@ export default class SettingsPage extends React.Component { bsStyle='link' style={settingsPage.close} onClick={this.handleCancel} - disabled={settings.mergeDefaultTeams(this.state.teams).length === 0} + disabled={this.state.localTeams.length === 0} > {'×'} @@ -625,9 +636,4 @@ export default class SettingsPage extends React.Component { } } -SettingsPage.propTypes = { - configFile: PropTypes.string, - enableServerManagement: PropTypes.bool, -}; - /* eslint-enable react/no-set-state */ diff --git a/src/browser/config/AppConfig.js b/src/browser/config/AppConfig.js deleted file mode 100644 index 775eeeeb..00000000 --- a/src/browser/config/AppConfig.js +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2015-2016 Yuya Ochiai -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {remote} from 'electron'; - -import settings from '../../common/settings'; - -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); - } -} - -export default new AppConfig(remote.app.getPath('userData') + '/config.json'); diff --git a/src/browser/index.jsx b/src/browser/index.jsx index 5b690b59..db9241a0 100644 --- a/src/browser/index.jsx +++ b/src/browser/index.jsx @@ -1,7 +1,6 @@ // Copyright (c) 2015-2016 Yuya Ochiai // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -'use strict'; import './css/index.css'; @@ -15,15 +14,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; import {remote, ipcRenderer} from 'electron'; -import buildConfig from '../common/config/buildConfig'; -import settings from '../common/settings'; import utils from '../utils/util'; +import Config from '../common/config'; + import MainPage from './components/MainPage.jsx'; -import AppConfig from './config/AppConfig.js'; import {createDataURL as createBadgeDataURL} from './js/badge'; -const teams = settings.mergeDefaultTeams(AppConfig.data.teams); +const config = new Config(remote.app.getPath('userData') + '/config.json'); + +const teams = config.teams; remote.getCurrentWindow().removeAllListeners('focus'); @@ -31,6 +31,30 @@ if (teams.length === 0) { remote.getCurrentWindow().loadFile('browser/settings.html'); } +const permissionRequestQueue = []; +const requestingPermission = new Array(teams.length); + +const parsedURL = url.parse(window.location.href, true); +const initialIndex = parsedURL.query.index ? parseInt(parsedURL.query.index, 10) : 0; + +let deeplinkingUrl = null; +if (!parsedURL.query.index || parsedURL.query.index === null) { + deeplinkingUrl = remote.getCurrentWindow().deeplinkingUrl; +} + +config.on('update', (configData) => { + teams.splice(0, teams.length, ...configData.teams); + requestingPermission.length = teams.length; +}); + +config.on('synchronize', () => { + ipcRenderer.send('reload-config'); +}); + +ipcRenderer.on('reload-config', () => { + config.reload(); +}); + function showBadgeWindows(sessionExpired, unreadCount, mentionCount) { function sendBadge(dataURL, description) { // window.setOverlayIcon() does't work with NativeImage across remote boundaries. @@ -50,7 +74,7 @@ function showBadgeWindows(sessionExpired, unreadCount, mentionCount) { } else if (mentionCount > 0) { const dataURL = createBadgeDataURL(mentionCount.toString()); sendBadge(dataURL, 'You have unread mentions (' + mentionCount + ')'); - } else if (unreadCount > 0 && AppConfig.data.showUnreadBadge) { + } else if (unreadCount > 0 && config.showUnreadBadge) { const dataURL = createBadgeDataURL('•'); sendBadge(dataURL, 'You have unread channels (' + unreadCount + ')'); } else { @@ -63,7 +87,7 @@ function showBadgeOSX(sessionExpired, unreadCount, mentionCount) { remote.app.dock.setBadge('•'); } else if (mentionCount > 0) { remote.app.dock.setBadge(mentionCount.toString()); - } else if (unreadCount > 0 && AppConfig.data.showUnreadBadge) { + } else if (unreadCount > 0 && config.showUnreadBadge) { remote.app.dock.setBadge('•'); } else { remote.app.dock.setBadge(''); @@ -106,15 +130,8 @@ function showBadge(sessionExpired, unreadCount, mentionCount) { } } -const permissionRequestQueue = []; -const requestingPermission = new Array(AppConfig.data.teams.length); - function teamConfigChange(updatedTeams) { - AppConfig.set('teams', updatedTeams.slice(buildConfig.defaultTeams.length)); - teams.splice(0, teams.length, ...updatedTeams); - requestingPermission.length = teams.length; - ipcRenderer.send('update-menu', AppConfig.data); - ipcRenderer.send('update-config'); + config.set('teams', updatedTeams); } function feedPermissionRequest() { @@ -152,6 +169,10 @@ function handleClickPermissionDialog(index, status) { feedPermissionRequest(); } +function handleSelectSpellCheckerLocale(locale) { + config.set('spellCheckerLocale', locale); +} + ipcRenderer.on('request-permission', (event, origin, permission) => { if (permissionRequestQueue.length >= 100) { return; @@ -160,31 +181,16 @@ ipcRenderer.on('request-permission', (event, origin, permission) => { feedPermissionRequest(); }); -function handleSelectSpellCheckerLocale(locale) { - console.log(locale); - AppConfig.set('spellCheckerLocale', locale); - ipcRenderer.send('update-config'); - ipcRenderer.send('update-dict'); -} - -const parsedURL = url.parse(window.location.href, true); -const initialIndex = parsedURL.query.index ? parseInt(parsedURL.query.index, 10) : 0; - -let deeplinkingUrl = null; -if (!parsedURL.query.index || parsedURL.query.index === null) { - deeplinkingUrl = remote.getCurrentWindow().deeplinkingUrl; -} - ReactDOM.render( , diff --git a/src/browser/settings.jsx b/src/browser/settings.jsx index 7087898e..a8051c4b 100644 --- a/src/browser/settings.jsx +++ b/src/browser/settings.jsx @@ -1,7 +1,6 @@ // Copyright (c) 2015-2016 Yuya Ochiai // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -'use strict'; import {remote} from 'electron'; window.eval = global.eval = () => { // eslint-disable-line no-multi-assign, no-eval @@ -11,20 +10,13 @@ window.eval = global.eval = () => { // eslint-disable-line no-multi-assign, no-e import React from 'react'; import ReactDOM from 'react-dom'; -import buildConfig from '../common/config/buildConfig'; - import SettingsPage from './components/SettingsPage.jsx'; import contextMenu from './js/contextMenu'; -const configFile = remote.app.getPath('userData') + '/config.json'; - contextMenu.setup(remote.getCurrentWindow()); ReactDOM.render( - , + , document.getElementById('content') ); diff --git a/src/common/config/index.js b/src/common/config/index.js new file mode 100644 index 00000000..457e1e63 --- /dev/null +++ b/src/common/config/index.js @@ -0,0 +1,354 @@ +// Copyright (c) 2015-2016 Yuya Ochiai +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import fs from 'fs'; +import path from 'path'; + +import {EventEmitter} from 'events'; + +import defaultPreferences from './defaultPreferences'; +import upgradeConfigData from './upgradePreferences'; +import buildConfig from './buildConfig'; + +/** + * Handles loading and merging all sources of configuration as well as saving user provided config + */ +export default class Config extends EventEmitter { + constructor(configFilePath) { + super(); + this.configFilePath = configFilePath; + this.reload(); + } + + /** + * Reload all sources of config data + * + * @param {boolean} synchronize determines whether or not to emit a synchronize event once config has been reloaded + * @emits {update} emitted once all data has been loaded and merged + * @emits {synchronize} emitted when requested by a call to method; used to notify other config instances of changes + */ + reload(synchronize = false) { + this.defaultConfigData = this.loadDefaultConfigData(); + this.buildConfigData = this.loadBuildConfigData(); + + this.localConfigData = this.loadLocalConfigFile(); + this.localConfigData = this.checkForConfigUpdates(this.localConfigData); + + this.GPOConfigData = this.loadGPOConfigData(); + + this.regenerateCombinedConfigData(); + + this.emit('update', this.combinedData); + + if (synchronize) { + this.emit('synchronize'); + } + } + + /** + * Used to save a single config property + * + * @param {string} key name of config property to be saved + * @param {*} data value to save for provided key + */ + set(key, data) { + if (key) { + this.localConfigData[key] = data; + this.regenerateCombinedConfigData(); + this.saveLocalConfigData(); + } + } + + /** + * Used to save an array of config properties in one go + * + * @param {array} properties an array of config properties to save + */ + setMultiple(properties = []) { + if (properties.length) { + properties.forEach(({key, data}) => { + if (key) { + this.localConfigData[key] = data; + } + }); + this.regenerateCombinedConfigData(); + this.saveLocalConfigData(); + } + } + + /** + * Used to replace the existing config data with new config data + * + * @param {object} configData a new, config data object to completely replace the existing config data + */ + replace(configData) { + const newConfigData = configData; + + this.localConfigData = Object.assign({}, this.localConfigData, newConfigData); + + this.regenerateCombinedConfigData(); + this.saveLocalConfigData(); + } + + /** + * Used to save the current set of local config data to disk + * + * @emits {update} emitted once all data has been saved + * @emits {synchronize} emitted once all data has been saved; used to notify other config instances of changes + * @emits {error} emitted if saving local config data to file fails + */ + saveLocalConfigData() { + try { + this.writeFile(this.configFilePath, this.localConfigData, (error) => { + if (error) { + throw new Error(error); + } + this.emit('update', this.combinedData); + this.emit('synchronize'); + }); + } catch (error) { + this.emit('error', error); + } + } + + // getters for accessing the various config data inputs + + get data() { + return this.combinedData; + } + get localData() { + return this.localConfigData; + } + get defaultData() { + return this.defaultConfigData; + } + get buildData() { + return this.buildConfigData; + } + get GPOData() { + return this.GPOConfigData; + } + + // convenience getters + + get version() { + return this.combinedData.version; + } + get teams() { + return this.combinedData.teams; + } + get localTeams() { + return this.localConfigData.teams; + } + get predefinedTeams() { + return [...this.buildConfigData.defaultTeams, ...this.GPOConfigData.teams]; + } + get enableHardwareAcceleration() { + return this.combinedData.enableHardwareAcceleration; + } + get enableServerManagement() { + return this.combinedData.enableServerManagement; + } + get enableAutoUpdater() { + return this.combinedData.enableAutoUpdater; + } + get autostart() { + return this.combinedData.autostart; + } + get notifications() { + return this.combinedData.notifications; + } + get showUnreadBadge() { + return this.combinedData.showUnreadBadge; + } + get useSpellChecker() { + return this.combinedData.useSpellChecker; + } + get spellCheckerLocale() { + return this.combinedData.spellCheckerLocale; + } + get showTrayIcon() { + return this.combinedData.showTrayIcon; + } + get trayIconTheme() { + return this.combinedData.trayIconTheme; + } + get helpLink() { + return this.combinedData.helpLink; + } + + // initialization/processing methods + + /** + * Returns a copy of the app's default config data + */ + loadDefaultConfigData() { + return this.copy(defaultPreferences); + } + + /** + * Returns a copy of the app's build config data + */ + loadBuildConfigData() { + return this.copy(buildConfig); + } + + /** + * Loads and returns locally stored config data from the filesystem or returns app defaults if no file is found + */ + loadLocalConfigFile() { + let configData = {}; + try { + configData = this.readFileSync(this.configFilePath); + } catch (e) { + console.log('Failed to load configuration file from the filesystem. Using defaults.'); + configData = this.copy(this.defaultConfigData); + + // add default team to teams if one exists and there arent currently any teams + if (!configData.teams.length && this.defaultConfigData.defaultTeam) { + configData.teams.push(this.defaultConfigData.defaultTeam); + } + delete configData.defaultTeam; + + this.writeFileSync(this.configFilePath, configData); + } + return configData; + } + + /** + * Loads and returns config data defined in GPO for Windows + */ + loadGPOConfigData() { + const configData = { + teams: [], + enableServerManagement: true, + enableAutoUpdater: true, + }; + if (process.platform === 'win32') { + // + // TODO: GPO data needs to be retrieved here and merged into the local `configData` variable for return + // + } + return configData; + } + + /** + * Determines if locally stored data needs to be updated and upgrades as needed + * + * @param {*} data locally stored data + */ + checkForConfigUpdates(data) { + let configData = data; + try { + if (configData.version !== this.defaultConfigData.version) { + configData = upgradeConfigData(configData); + this.writeFileSync(this.configFilePath, configData); + console.log(`Configuration updated to version ${this.defaultConfigData.version} successfully.`); + } + } catch (error) { + console.log(`Failed to update configuration to version ${this.defaultConfigData.version}.`); + } + return configData; + } + + /** + * Properly combines all sources of data into a single, manageable set of all config data + */ + regenerateCombinedConfigData() { + // combine all config data in the correct order + this.combinedData = Object.assign({}, this.defaultConfigData, this.localConfigData, this.buildConfigData, this.GPOConfigData); + + // remove unecessary data pulled from default and build config + delete this.combinedData.defaultTeam; + delete this.combinedData.defaultTeams; + + // IMPORTANT: properly combine teams from all sources + const combinedTeams = []; + + // - start by adding default teams from buildConfig, if any + if (this.buildConfigData.defaultTeams && this.buildConfigData.defaultTeams.length) { + combinedTeams.push(...this.buildConfigData.defaultTeams); + } + + // - add GPO defined teams, if any + if (this.GPOConfigData.teams && this.GPOConfigData.teams.length) { + combinedTeams.push(...this.GPOConfigData.teams); + } + + // - add locally defined teams only if server management is enabled + if (this.enableServerManagement) { + combinedTeams.push(...this.localConfigData.teams); + } + + this.combinedData.teams = combinedTeams; + this.combinedData.localTeams = this.localConfigData.teams; + this.combinedData.buildTeams = this.buildConfigData.defaultTeams; + this.combinedData.GPOTeams = this.GPOConfigData.teams; + } + + /** + * Returns the provided list of teams with duplicates filtered out + * + * @param {array} teams array of teams to check for duplicates + */ + filterOutDuplicateTeams(teams) { + let newTeams = teams; + const uniqueURLs = new Set(); + newTeams = newTeams.filter((team) => { + return uniqueURLs.has(team.url) ? false : uniqueURLs.add(team.url); + }); + return newTeams; + } + + /** + * Returns the provided array fo teams with existing teams filtered out + * @param {array} teams array of teams to check for already defined teams + */ + filterOutPredefinedTeams(teams) { + let newTeams = teams; + + // filter out predefined teams + newTeams = newTeams.filter((newTeam) => { + return this.predefinedTeams.findIndex((existingTeam) => newTeam.url === existingTeam.url) === -1; // eslint-disable-line max-nested-callbacks + }); + + return newTeams; + } + + // helper functions + + readFileSync(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } + + writeFile(filePath, configData, callback) { + if (configData.version !== this.defaultConfigData.version) { + throw new Error('version ' + configData.version + ' is not equal to ' + this.defaultConfigData.version); + } + const json = JSON.stringify(configData, null, ' '); + fs.writeFile(filePath, json, 'utf8', callback); + } + + writeFileSync(filePath, config) { + if (config.version !== this.defaultConfigData.version) { + throw new Error('version ' + config.version + ' is not equal to ' + this.defaultConfigData.version); + } + + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + + const json = JSON.stringify(config, null, ' '); + fs.writeFileSync(filePath, json, 'utf8'); + } + + merge(base, target) { + return Object.assign({}, base, target); + } + + copy(data) { + return Object.assign({}, data); + } +} diff --git a/src/common/settings.js b/src/common/settings.js deleted file mode 100644 index 0327c61c..00000000 --- a/src/common/settings.js +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2015-2016 Yuya Ochiai -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -'use strict'; - -import fs from 'fs'; -import path from 'path'; - -import buildConfig from './config/buildConfig'; - -function merge(base, target) { - return Object.assign({}, base, target); -} - -import defaultPreferences from './config/defaultPreferences'; -import upgradePreferences from './config/upgradePreferences'; - -function loadDefault() { - return JSON.parse(JSON.stringify(defaultPreferences)); -} - -function hasBuildConfigDefaultTeams(config) { - return config.defaultTeams.length > 0; -} - -function upgrade(config) { - return upgradePreferences(config); -} - -export default { - version: defaultPreferences.version, - - upgrade, - - readFileSync(configFile) { - const config = JSON.parse(fs.readFileSync(configFile, 'utf8')); - if (config.version === defaultPreferences.version) { - const defaultConfig = loadDefault(); - return merge(defaultConfig, config); - } - return config; - }, - - writeFile(configFile, config, callback) { - if (config.version !== defaultPreferences.version) { - throw new Error('version ' + config.version + ' is not equal to ' + defaultPreferences.version); - } - const data = JSON.stringify(config, null, ' '); - fs.writeFile(configFile, data, 'utf8', callback); - }, - - writeFileSync(configFile, config) { - if (config.version !== defaultPreferences.version) { - throw new Error('version ' + config.version + ' is not equal to ' + defaultPreferences.version); - } - - const dir = path.dirname(configFile); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - } - - const data = JSON.stringify(config, null, ' '); - fs.writeFileSync(configFile, data, 'utf8'); - }, - - loadDefault, - - mergeDefaultTeams(teams) { - const newTeams = []; - if (hasBuildConfigDefaultTeams(buildConfig)) { - newTeams.push(...JSON.parse(JSON.stringify(buildConfig.defaultTeams))); - } - if (buildConfig.enableServerManagement) { - newTeams.push(...JSON.parse(JSON.stringify(teams))); - } - return newTeams; - }, -}; diff --git a/src/main.js b/src/main.js index 23ff53cd..9ba1cb44 100644 --- a/src/main.js +++ b/src/main.js @@ -1,7 +1,6 @@ // Copyright (c) 2015-2016 Yuya Ochiai // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -'use strict'; import os from 'os'; import path from 'path'; @@ -29,7 +28,6 @@ import AutoLauncher from './main/AutoLauncher'; import CriticalErrorHandler from './main/CriticalErrorHandler'; import upgradeAutoLaunch from './main/autoLaunch'; import autoUpdater from './main/autoUpdater'; -import buildConfig from './common/config/buildConfig'; const criticalErrorHandler = new CriticalErrorHandler(); @@ -39,7 +37,7 @@ global.willAppQuit = false; app.setAppUserModelId('com.squirrel.mattermost.Mattermost'); // Use explicit AppUserModelID -import settings from './common/settings'; +import Config from './common/config'; import CertificateStore from './main/certificateStore'; const certificateStore = CertificateStore.load(path.resolve(app.getPath('userData'), 'certificate.json')); import createMainWindow from './main/mainWindow'; @@ -65,6 +63,7 @@ let deeplinkingUrl = null; let scheme = null; let appState = null; let permissionManager = null; +let config = null; const argv = parseArgv(process.argv.slice(1)); const hideOnStartup = shouldBeHiddenOnStartup(argv); @@ -75,31 +74,14 @@ if (argv['data-dir']) { global.isDev = isDev && !argv.disableDevMode; -let config = {}; -try { - const configFile = app.getPath('userData') + '/config.json'; - config = settings.readFileSync(configFile); - if (config.version !== settings.version) { - config = settings.upgrade(config); - settings.writeFileSync(configFile, config); - } -} catch (e) { - config = settings.loadDefault(); - console.log('Failed to read or upgrade config.json', e); - if (!config.teams.length && config.defaultTeam) { - config.teams.push(config.defaultTeam); +config = new Config(app.getPath('userData') + '/config.json'); - const configFile = app.getPath('userData') + '/config.json'; - settings.writeFileSync(configFile, config); - } -} +// can only call this before the app is ready if (config.enableHardwareAcceleration === false) { app.disableHardwareAcceleration(); } -ipcMain.on('update-config', () => { - const configFile = app.getPath('userData') + '/config.json'; - config = settings.readFileSync(configFile); +config.on('update', (configData) => { if (process.platform === 'win32' || process.platform === 'linux') { const appLauncher = new AutoLauncher(); const autoStartTask = config.autostart ? appLauncher.enable() : appLauncher.disable(); @@ -109,9 +91,26 @@ ipcMain.on('update-config', () => { console.log('error:', err); }); } - const trustedURLs = settings.mergeDefaultTeams(config.teams).map((team) => team.url); - permissionManager.setTrustedURLs(trustedURLs); - ipcMain.emit('update-dict', true, config.spellCheckerLocale); + + if (permissionManager) { + const trustedURLs = config.teams.map((team) => team.url); + permissionManager.setTrustedURLs(trustedURLs); + ipcMain.emit('update-dict', true, config.spellCheckerLocale); + } + + ipcMain.emit('update-menu', true, configData); +}); + +// when the config object changes here in the main process, tell the renderer process to reload any initialized config objects to get the changes +config.on('synchronize', () => { + if (mainWindow) { + mainWindow.webContents.send('reload-config'); + } +}); + +// listen for any config reload requests from the renderer process to reload configuration changes here in the main process +ipcMain.on('reload-config', () => { + config.reload(); }); // Only for OS X @@ -419,9 +418,7 @@ app.on('ready', () => { } if (!config.spellCheckerLocale) { - config.spellCheckerLocale = SpellChecker.getSpellCheckerLocale(app.getLocale()); - const configFile = app.getPath('userData') + '/config.json'; - settings.writeFileSync(configFile, config); + config.set('spellCheckerLocale', SpellChecker.getSpellCheckerLocale(app.getLocale())); } const appStateJson = path.join(app.getPath('userData'), 'app-state.json'); @@ -455,7 +452,7 @@ app.on('ready', () => { initCookieManager(session.defaultSession); - mainWindow = createMainWindow(config, { + mainWindow = createMainWindow(config.data, { hideOnStartup, linuxAppIcon: path.join(assetsDir, 'appicon.png'), deeplinkingUrl, @@ -618,7 +615,7 @@ app.on('ready', () => { } } }); - ipcMain.emit('update-menu', true, config); + ipcMain.emit('update-menu', true, config.data); ipcMain.on('update-dict', () => { if (config.useSpellChecker) { @@ -665,11 +662,11 @@ app.on('ready', () => { ipcMain.emit('update-dict'); const permissionFile = path.join(app.getPath('userData'), 'permission.json'); - const trustedURLs = settings.mergeDefaultTeams(config.teams).map((team) => team.url); + const trustedURLs = config.teams.map((team) => team.url); permissionManager = new PermissionManager(permissionFile, trustedURLs); session.defaultSession.setPermissionRequestHandler(permissionRequestHandler(mainWindow, permissionManager)); - if (buildConfig.enableAutoUpdater) { + if (config.enableAutoUpdater) { const updaterConfig = autoUpdater.loadConfig(); autoUpdater.initialize(appState, mainWindow, updaterConfig.isNotifyOnly()); ipcMain.on('check-for-updates', autoUpdater.checkForUpdates); @@ -694,7 +691,7 @@ app.on('web-contents-created', (dc, contents) => { }); contents.on('will-navigate', (event, navigationUrl) => { const parsedUrl = new URL(navigationUrl); - const trustedURLs = settings.mergeDefaultTeams(config.teams).map((team) => new URL(team.url)); //eslint-disable-line max-nested-callbacks + const trustedURLs = config.teams.map((team) => new URL(team.url)); //eslint-disable-line max-nested-callbacks let trusted = false; for (const url of trustedURLs) { diff --git a/src/main/menus/app.js b/src/main/menus/app.js index 147d47f1..3da62b41 100644 --- a/src/main/menus/app.js +++ b/src/main/menus/app.js @@ -5,9 +5,6 @@ import {app, dialog, ipcMain, Menu, shell} from 'electron'; -import settings from '../../common/settings'; -import buildConfig from '../../common/config/buildConfig'; - function createTemplate(mainWindow, config, isDev) { const settingsURL = isDev ? 'http://localhost:8080/browser/settings.html' : `file://${app.getAppPath()}/browser/settings.html`; @@ -42,7 +39,7 @@ function createTemplate(mainWindow, config, isDev) { }, }]; - if (buildConfig.enableServerManagement === true) { + if (config.enableServerManagement === true) { platformAppMenu.push({ label: 'Sign in to Another Server', click() { @@ -189,7 +186,7 @@ function createTemplate(mainWindow, config, isDev) { }], }); - const teams = settings.mergeDefaultTeams(config.teams); + const teams = config.teams; const windowMenu = { label: '&Window', submenu: [{ @@ -223,11 +220,11 @@ function createTemplate(mainWindow, config, isDev) { }; template.push(windowMenu); const submenu = []; - if (buildConfig.helpLink) { + if (config.helpLink) { submenu.push({ label: 'Learn More...', click() { - shell.openExternal(buildConfig.helpLink); + shell.openExternal(config.helpLink); }, }); submenu.push(separatorItem); @@ -236,7 +233,7 @@ function createTemplate(mainWindow, config, isDev) { label: `Version ${app.getVersion()}`, enabled: false, }); - if (buildConfig.enableAutoUpdater) { + if (config.enableAutoUpdater) { submenu.push({ label: 'Check for Updates...', click() { diff --git a/src/main/menus/tray.js b/src/main/menus/tray.js index 17ff126d..978b9e45 100644 --- a/src/main/menus/tray.js +++ b/src/main/menus/tray.js @@ -5,11 +5,9 @@ import {app, Menu} from 'electron'; -import settings from '../../common/settings'; - function createTemplate(mainWindow, config, isDev) { const settingsURL = isDev ? 'http://localhost:8080/browser/settings.html' : `file://${app.getAppPath()}/browser/settings.html`; - const teams = settings.mergeDefaultTeams(config.teams); + const teams = config.teams; const template = [ ...teams.slice(0, 9).map((team, i) => { return { diff --git a/src/package-lock.json b/src/package-lock.json index 57952f61..27c35efe 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -171,10 +171,6 @@ "resolved": "https://registry.npmjs.org/cross-unzip/-/cross-unzip-0.0.2.tgz", "integrity": "sha1-UYO8R6CVWb78+YzEZXlkmZNZNy8=" }, - "damerau-levenshtein": { - "version": "git://github.com/cbaatz/damerau-levenshtein.git#8d7440a51ae9dc6912e44385115c7cb1da4e8ebc", - "from": "git://github.com/cbaatz/damerau-levenshtein.git" - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -642,9 +638,14 @@ "integrity": "sha512-RGgpBBn4p65nOhZdBHDt84Remz3QvmoQDppKJX0tYS53SbuUJwm7zG1swT5O770zPvN6wZaZlc8ie/jwlZbeHA==", "requires": { "binarysearch": "^0.2.4", - "damerau-levenshtein": "git://github.com/cbaatz/damerau-levenshtein.git", "strip-bom": "^2.0.0", "unzip-stream": "^0.3.0" + }, + "dependencies": { + "damerau-levenshtein": { + "version": "git://github.com/cbaatz/damerau-levenshtein.git#8d7440a51ae9dc6912e44385115c7cb1da4e8ebc", + "from": "git://github.com/cbaatz/damerau-levenshtein.git#8d7440a51ae9dc6912e44385115c7cb1da4e8ebc" + } } }, "sort-keys": { diff --git a/test/specs/app_test.js b/test/specs/app_test.js index 1ed1e52d..133d5af8 100644 --- a/test/specs/app_test.js +++ b/test/specs/app_test.js @@ -82,7 +82,8 @@ describe('application', function desc() { }); it('should upgrade v0 config file', async () => { - const settings = require('../../src/common/settings').default; + const Config = require('../../src/common/config').default; + const config = new Config(env.configFilePath); fs.writeFileSync(env.configFilePath, JSON.stringify({ url: env.mattermostURL, })); @@ -92,8 +93,8 @@ describe('application', function desc() { url.should.match(/\/index.html$/); const str = fs.readFileSync(env.configFilePath, 'utf8'); - const config = JSON.parse(str); - config.version.should.equal(settings.version); + const localConfigData = JSON.parse(str); + localConfigData.version.should.equal(config.defaultData.version); }); it.skip('should be stopped when the app instance already exists', (done) => { diff --git a/test/specs/browser/settings_test.js b/test/specs/browser/settings_test.js index 7daeb1ec..725c1fee 100644 --- a/test/specs/browser/settings_test.js +++ b/test/specs/browser/settings_test.js @@ -216,18 +216,18 @@ describe('browser/settings.html', function desc() { await this.app.client. loadSettingsPage(). click('#inputShowTrayIcon'). - click('input[value="light"]'). - pause(700); // wait auto-save - - const config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8')); - config0.trayIconTheme.should.equal('light'); - - await this.app.client. click('input[value="dark"]'). pause(700); // wait auto-save + const config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8')); + config0.trayIconTheme.should.equal('dark'); + + await this.app.client. + click('input[value="light"]'). + pause(700); // wait auto-save + const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8')); - config1.trayIconTheme.should.equal('dark'); + config1.trayIconTheme.should.equal('light'); }); }); }); diff --git a/test/specs/settings_test.js b/test/specs/settings_test.js deleted file mode 100644 index 0c508bcd..00000000 --- a/test/specs/settings_test.js +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2015-2016 Yuya Ochiai -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import settings from '../../src/common/settings'; -import buildConfig from '../../src/common/config/buildConfig'; -import defaultPreferences from '../../src/common/config/defaultPreferences'; -import pastDefaultPreferences from '../../src/common/config/pastDefaultPreferences'; - -describe('common/settings.js', () => { - it('should upgrade v0 config file', () => { - const v0Config = { - url: 'https://example.com/team', - }; - const config = settings.upgrade(v0Config); - config.teams.length.should.equal(1); - config.teams[0].url.should.equal(v0Config.url); - config.version.should.equal(settings.version); - }); - - it('should merge teams with buildConfig.defaultTeams', () => { - const teams = [ - { - name: 'test', - url: 'https://example.com', - }, - ]; - - const mergedTeams = settings.mergeDefaultTeams(teams); - mergedTeams.should.deep.equal([ - { - name: 'test', - url: 'https://example.com', - }, - ...buildConfig.defaultTeams, - ]); - }); -}); - -describe('common/config/', () => { - it('pastDefaultPreferences should have each past version of defaultPreferences', () => { - for (let version = 0; version <= defaultPreferences.version; version++) { - pastDefaultPreferences[`${version}`].should.exist; - } - }); - - it('defaultPreferences equal to one of pastDefaultPreferences', () => { - const pastPreferences = pastDefaultPreferences[`${defaultPreferences.version}`]; - pastPreferences.should.deep.equal(defaultPreferences); - }); -});