diff --git a/e2e/specs/focus.test.js b/e2e/specs/focus.test.js index 7e9dadb7..7b3ccc36 100644 --- a/e2e/specs/focus.test.js +++ b/e2e/specs/focus.test.js @@ -67,14 +67,14 @@ describe('focus', function desc() { }); describe('Focus textbox tests', () => { - it('MM-T1315 should return focus to the message box when closing the settings window', async () => { + it('MM-T1315 should return focus to the message box when closing the settings modal', async () => { this.app.evaluate(({ipcMain}, showWindow) => { ipcMain.emit(showWindow); }, SHOW_SETTINGS_WINDOW); const settingsWindow = await this.app.waitForEvent('window', { predicate: (window) => window.url().includes('settings'), }); - await settingsWindow.waitForSelector('.settingsPage.container'); + await settingsWindow.waitForSelector('.SettingsModal'); await settingsWindow.close(); const isTextboxFocused = await firstServer.$eval('#post_textbox', (el) => el === document.activeElement); @@ -91,7 +91,7 @@ describe('focus', function desc() { textboxString.should.equal('Mattermost'); }); - it('MM-T1316 should return focus to the message box when closing the settings window', async () => { + it('MM-T1316 should return focus to the message box when closing the Add Server modal', async () => { const mainView = this.app.windows().find((window) => window.url().includes('index')); const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown')); await mainView.click('.ServerDropdownButton'); diff --git a/e2e/specs/settings.test.js b/e2e/specs/settings.test.js index f3d5e624..bd0a09ca 100644 --- a/e2e/specs/settings.test.js +++ b/e2e/specs/settings.test.js @@ -42,9 +42,10 @@ describe('Settings', function desc() { const settingsWindow = await this.app.waitForEvent('window', { predicate: (window) => window.url().includes('settings'), }); - await settingsWindow.waitForSelector('.settingsPage.container'); - await settingsWindow.waitForSelector('#inputAutoStart', {state: expected ? 'attached' : 'detached'}); - const existing = await settingsWindow.isVisible('#inputAutoStart'); + await settingsWindow.waitForSelector('#settingCategoryButton-general'); + await settingsWindow.click('#settingCategoryButton-general'); + await settingsWindow.waitForSelector('#CheckSetting_autostart', {state: expected ? 'attached' : 'detached'}); + const existing = await settingsWindow.isVisible('#CheckSetting_autostart'); existing.should.equal(expected); }); }); @@ -58,9 +59,10 @@ describe('Settings', function desc() { const settingsWindow = await this.app.waitForEvent('window', { predicate: (window) => window.url().includes('settings'), }); - await settingsWindow.waitForSelector('.settingsPage.container'); - await settingsWindow.waitForSelector('#inputShowTrayIcon', {state: expected ? 'attached' : 'detached'}); - const existing = await settingsWindow.isVisible('#inputShowTrayIcon'); + await settingsWindow.waitForSelector('#settingCategoryButton-general'); + await settingsWindow.click('#settingCategoryButton-general'); + await settingsWindow.waitForSelector('#CheckSetting_showTrayIcon', {state: expected ? 'attached' : 'detached'}); + const existing = await settingsWindow.isVisible('#CheckSetting_showTrayIcon'); existing.should.equal(expected); }); @@ -72,17 +74,18 @@ describe('Settings', function desc() { const settingsWindow = await this.app.waitForEvent('window', { predicate: (window) => window.url().includes('settings'), }); - await settingsWindow.waitForSelector('.settingsPage.container'); - await settingsWindow.click('#inputShowTrayIcon'); - await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")'); - await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")'); + await settingsWindow.waitForSelector('#settingCategoryButton-general'); + await settingsWindow.click('#settingCategoryButton-general'); + await settingsWindow.click('#CheckSetting_showTrayIcon button'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")'); let config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8')); config0.showTrayIcon.should.true; - await settingsWindow.click('#inputShowTrayIcon'); - await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")'); - await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")'); + await settingsWindow.click('#CheckSetting_showTrayIcon button'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")'); config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8')); config0.showTrayIcon.should.false; @@ -97,18 +100,20 @@ describe('Settings', function desc() { const settingsWindow = await this.app.waitForEvent('window', { predicate: (window) => window.url().includes('settings'), }); - await settingsWindow.waitForSelector('.settingsPage.container'); - await settingsWindow.click('#inputShowTrayIcon'); - await settingsWindow.click('input[value="dark"]'); - await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")'); - await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")'); + await settingsWindow.waitForSelector('#settingCategoryButton-general'); + await settingsWindow.click('#settingCategoryButton-general'); + await settingsWindow.click('#CheckSetting_showTrayIcon button'); + await settingsWindow.click('#RadioSetting_trayIconTheme_dark'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")'); const config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8')); config0.trayIconTheme.should.equal('dark'); - await settingsWindow.click('input[value="light"]'); - await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")'); - await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")'); + await settingsWindow.waitForSelector('.SettingsModal__saving', {state: 'detached'}); + await settingsWindow.click('#RadioSetting_trayIconTheme_light'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")'); const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8')); config1.trayIconTheme.should.equal('light'); @@ -125,8 +130,9 @@ describe('Settings', function desc() { const settingsWindow = await this.app.waitForEvent('window', { predicate: (window) => window.url().includes('settings'), }); - await settingsWindow.waitForSelector('.settingsPage.container'); - const existing = await settingsWindow.isVisible('#inputMinimizeToTray'); + await settingsWindow.waitForSelector('#settingCategoryButton-general'); + await settingsWindow.click('#settingCategoryButton-general'); + const existing = await settingsWindow.isVisible('#CheckSetting_minimizeToTray'); existing.should.equal(expected); }); }); @@ -140,8 +146,9 @@ describe('Settings', function desc() { const settingsWindow = await this.app.waitForEvent('window', { predicate: (window) => window.url().includes('settings'), }); - await settingsWindow.waitForSelector('.settingsPage.container'); - const existing = await settingsWindow.isVisible('#inputflashWindow'); + await settingsWindow.waitForSelector('#settingCategoryButton-notifications'); + await settingsWindow.click('#settingCategoryButton-notifications'); + const existing = await settingsWindow.isVisible('#CheckSetting_flashWindow'); existing.should.equal(expected); }); }); @@ -155,8 +162,9 @@ describe('Settings', function desc() { const settingsWindow = await this.app.waitForEvent('window', { predicate: (window) => window.url().includes('settings'), }); - await settingsWindow.waitForSelector('.settingsPage.container'); - const existing = await settingsWindow.isVisible('#inputShowUnreadBadge'); + await settingsWindow.waitForSelector('#settingCategoryButton-notifications'); + await settingsWindow.click('#settingCategoryButton-notifications'); + const existing = await settingsWindow.isVisible('#CheckSetting_showUnreadBadge'); existing.should.equal(expected); }); }); @@ -169,16 +177,17 @@ describe('Settings', function desc() { const settingsWindow = await this.app.waitForEvent('window', { predicate: (window) => window.url().includes('settings'), }); - await settingsWindow.waitForSelector('.settingsPage.container'); - const existing = await settingsWindow.isVisible('#inputSpellChecker'); + await settingsWindow.waitForSelector('#settingCategoryButton-language'); + await settingsWindow.click('#settingCategoryButton-language'); + const existing = await settingsWindow.isVisible('#CheckSetting_useSpellChecker'); existing.should.equal(true); - const selected = await settingsWindow.isChecked('#inputSpellChecker'); + const selected = await settingsWindow.isChecked('#checkSetting-useSpellChecker'); selected.should.equal(true); - await settingsWindow.click('#inputSpellChecker'); - await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")'); - await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")'); + await settingsWindow.click('#CheckSetting_useSpellChecker button'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")'); const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8')); config1.useSpellChecker.should.equal(false); @@ -187,26 +196,28 @@ describe('Settings', function desc() { describe('Enable GPU hardware acceleration', () => { it('MM-T4398 should save selected option', async () => { - const ID_INPUT_ENABLE_HARDWARE_ACCELERATION = '#inputEnableHardwareAcceleration'; + const ID_INPUT_ENABLE_HARDWARE_ACCELERATION = '#CheckSetting_enableHardwareAcceleration button'; this.app.evaluate(({ipcMain}, showWindow) => { ipcMain.emit(showWindow); }, SHOW_SETTINGS_WINDOW); const settingsWindow = await this.app.waitForEvent('window', { predicate: (window) => window.url().includes('settings'), }); - await settingsWindow.waitForSelector('.settingsPage.container'); - const selected = await settingsWindow.isChecked(ID_INPUT_ENABLE_HARDWARE_ACCELERATION); + await settingsWindow.waitForSelector('#settingCategoryButton-advanced'); + await settingsWindow.click('#settingCategoryButton-advanced'); + console.log('balls'); + const selected = await settingsWindow.isChecked('#checkSetting-enableHardwareAcceleration'); selected.should.equal(true); // default is true await settingsWindow.click(ID_INPUT_ENABLE_HARDWARE_ACCELERATION); - await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")'); - await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")'); const config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8')); config0.enableHardwareAcceleration.should.equal(false); await settingsWindow.click(ID_INPUT_ENABLE_HARDWARE_ACCELERATION); - await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")'); - await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")'); const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8')); config1.enableHardwareAcceleration.should.equal(true); }); @@ -215,26 +226,27 @@ describe('Settings', function desc() { if (process.platform !== 'darwin') { describe('Enable automatic check for updates', () => { it('MM-T4549 should save selected option', async () => { - const ID_INPUT_ENABLE_AUTO_UPDATES = '#inputAutoCheckForUpdates'; + const ID_INPUT_ENABLE_AUTO_UPDATES = '#CheckSetting_autoCheckForUpdates button'; this.app.evaluate(({ipcMain}, showWindow) => { ipcMain.emit(showWindow); }, SHOW_SETTINGS_WINDOW); const settingsWindow = await this.app.waitForEvent('window', { predicate: (window) => window.url().includes('settings'), }); - await settingsWindow.waitForSelector('.settingsPage.container'); - const selected = await settingsWindow.isChecked(ID_INPUT_ENABLE_AUTO_UPDATES); + await settingsWindow.waitForSelector('#settingCategoryButton-general'); + await settingsWindow.click('#settingCategoryButton-general'); + const selected = await settingsWindow.isChecked('#checkSetting-autoCheckForUpdates'); selected.should.equal(true); // default is true await settingsWindow.click(ID_INPUT_ENABLE_AUTO_UPDATES); - await settingsWindow.waitForSelector('.updatesSaveIndicator :text("Saving...")'); - await settingsWindow.waitForSelector('.updatesSaveIndicator :text("Saved")'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")'); const config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8')); config0.autoCheckForUpdates.should.equal(false); await settingsWindow.click(ID_INPUT_ENABLE_AUTO_UPDATES); - await settingsWindow.waitForSelector('.updatesSaveIndicator :text("Saving...")'); - await settingsWindow.waitForSelector('.updatesSaveIndicator :text("Saved")'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")'); + await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")'); const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8')); config1.autoCheckForUpdates.should.equal(true); }); diff --git a/e2e/specs/settings/keyboard_shortcuts.test.js b/e2e/specs/settings/keyboard_shortcuts.test.js index ee98d5b4..df866206 100644 --- a/e2e/specs/settings/keyboard_shortcuts.test.js +++ b/e2e/specs/settings/keyboard_shortcuts.test.js @@ -29,9 +29,10 @@ describe('settings/keyboard_shortcuts', function desc() { settingsWindow = await this.app.waitForEvent('window', { predicate: (window) => window.url().includes('settings'), }); - await settingsWindow.waitForSelector('.settingsPage.container'); + await settingsWindow.waitForSelector('#settingCategoryButton-language'); + await settingsWindow.click('#settingCategoryButton-language'); - const textbox = await settingsWindow.waitForSelector('#inputSpellCheckerLocalesDropdown'); + const textbox = await settingsWindow.waitForSelector('#selectSetting_spellCheckerLocales'); await textbox.scrollIntoViewIfNeeded(); }); @@ -45,22 +46,22 @@ describe('settings/keyboard_shortcuts', function desc() { describe('MM-T1288 Manipulating Text', () => { it('MM-T1288_1 should be able to select and deselect language in the settings window', async () => { let textboxString; - await settingsWindow.click('#inputSpellCheckerLocalesDropdown'); - await settingsWindow.type('#inputSpellCheckerLocalesDropdown', 'Afrikaans'); + await settingsWindow.click('#selectSetting_spellCheckerLocales'); + await settingsWindow.type('#selectSetting_spellCheckerLocales', 'Afrikaans'); robot.keyTap('tab'); - await settingsWindow.isVisible('#appOptionsSaveIndicator'); + await settingsWindow.isVisible('.SettingsModal__saving'); - textboxString = await settingsWindow.innerText('div.SettingsPage__spellCheckerLocalesDropdown__multi-value__label'); + textboxString = await settingsWindow.innerText('.SpellCheckerSetting .SelectSetting__select__multi-value__label'); textboxString.should.equal('Afrikaans'); - await settingsWindow.isVisible('#appOptionsSaveIndicator'); + await settingsWindow.isVisible('.SettingsModal__saving'); await settingsWindow.click('[aria-label="Remove Afrikaans"]'); - await settingsWindow.isVisible('#appOptionsSaveIndicator'); + await settingsWindow.isVisible('.SettingsModal__saving'); - textboxString = await settingsWindow.inputValue('#inputSpellCheckerLocalesDropdown'); + textboxString = await settingsWindow.inputValue('#selectSetting_spellCheckerLocales'); textboxString.should.equal(''); }); @@ -68,7 +69,7 @@ describe('settings/keyboard_shortcuts', function desc() { const textToCopy = 'Afrikaans'; env.clipboard(textToCopy); - const textbox = await settingsWindow.waitForSelector('#inputSpellCheckerLocalesDropdown'); + const textbox = await settingsWindow.waitForSelector('#selectSetting_spellCheckerLocales'); await textbox.selectText({force: true}); robot.keyTap('x', [env.cmdOrCtrl]); @@ -85,7 +86,7 @@ describe('settings/keyboard_shortcuts', function desc() { const textToCopy = 'Afrikaans'; env.clipboard(textToCopy); - const textbox = await settingsWindow.waitForSelector('#inputSpellCheckerLocalesDropdown'); + const textbox = await settingsWindow.waitForSelector('#selectSetting_spellCheckerLocales'); await textbox.selectText({force: true}); robot.keyTap('c', [env.cmdOrCtrl]); diff --git a/i18n/en.json b/i18n/en.json index 8db31408..2fa733b8 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -148,8 +148,6 @@ "main.windows.mainWindow.minimizeToTray.dialog.title": "Minimize to Tray", "modal.cancel": "Cancel", "modal.confirm": "Confirm", - "renderer.components.autoSaveIndicator.saved": "Saved", - "renderer.components.autoSaveIndicator.saving": "Saving...", "renderer.components.configureServer.cardtitle": "Enter your server details", "renderer.components.configureServer.connect.default": "Connect", "renderer.components.configureServer.connect.override": "Connect anyway", @@ -212,20 +210,20 @@ "renderer.components.saveButton.save": "Save", "renderer.components.saveButton.saving": "Saving", "renderer.components.serverDropdownButton.noServersConfigured": "No servers configured", + "renderer.components.settingsPage.advanced": "Advanced", "renderer.components.settingsPage.afterRestart": "Setting takes effect after restarting the app.", - "renderer.components.settingsPage.appLanguage": "Set app language (beta)", - "renderer.components.settingsPage.appLanguage.description": "Chooses the language that the Desktop App will use for menu items and popups. Still in beta, some languages will be missing translation strings.", - "renderer.components.settingsPage.appLanguage.useSystemDefault": "Use system default", - "renderer.components.settingsPage.appOptions": "App Options", - "renderer.components.settingsPage.bounceIcon": "Bounce the Dock icon", - "renderer.components.settingsPage.bounceIcon.description": "If enabled, the Dock icon bounces once or until the user opens the app when a new notification is received.", - "renderer.components.settingsPage.bounceIcon.once": "once", - "renderer.components.settingsPage.bounceIcon.untilOpenApp": "until I open the app", + "renderer.components.settingsPage.appLanguage": "App Language", + "renderer.components.settingsPage.appLanguage.description": "The language that the Desktop App will use for menu items and popups. Still in beta, some languages will be missing translation strings.", + "renderer.components.settingsPage.appLanguage.placeholder": "Use system default", + "renderer.components.settingsPage.bounceIcon.never": "Never", + "renderer.components.settingsPage.bounceIcon.once": "Once", + "renderer.components.settingsPage.bounceIcon.untilOpenApp": "Until I open the app", + "renderer.components.settingsPage.bounceIconType": "Bounce the Dock icon...", + "renderer.components.settingsPage.changesSaved": "Changes saved", "renderer.components.settingsPage.checkSpelling": "Check spelling", "renderer.components.settingsPage.checkSpelling.description": "Highlight misspelled words in your messages based on your system language or language preference.", "renderer.components.settingsPage.checkSpelling.editSpellcheckUrl": "Use an alternative dictionary URL", "renderer.components.settingsPage.checkSpelling.preferredLanguages": "Select preferred language(s)", - "renderer.components.settingsPage.checkSpelling.revertToDefault": "Revert to default", "renderer.components.settingsPage.checkSpelling.specifyURL": "Specify the url where dictionary definitions can be retrieved", "renderer.components.settingsPage.downloadLocation": "Download Location", "renderer.components.settingsPage.downloadLocation.description": "Specify the folder where files will download.", @@ -237,15 +235,15 @@ "renderer.components.settingsPage.flashWindow.description": "If enabled, the taskbar icon will flash for a few seconds when a new message is received.", "renderer.components.settingsPage.flashWindow.description.linuxFunctionality": "This functionality may not work with all Linux window managers.", "renderer.components.settingsPage.flashWindow.description.note": "NOTE: ", - "renderer.components.settingsPage.fullscreen": "Open app in fullscreen", + "renderer.components.settingsPage.fullscreen": "Open app in full screen", "renderer.components.settingsPage.fullscreen.description": "If enabled, the {appName} application will always open in full screen", - "renderer.components.settingsPage.header": "Settings", + "renderer.components.settingsPage.general": "General", + "renderer.components.settingsPage.header": "Desktop App Settings", + "renderer.components.settingsPage.language": "Language", "renderer.components.settingsPage.launchAppMinimized": "Launch app minimized", "renderer.components.settingsPage.launchAppMinimized.description": "If enabled, the app will start in system tray, and will not show the window on launch.", - "renderer.components.settingsPage.loadingConfig": "Loading configuration...", "renderer.components.settingsPage.loggingLevel": "Logging level", "renderer.components.settingsPage.loggingLevel.description": "Logging is helpful for developers and support to isolate issues you may be encountering with the desktop app.", - "renderer.components.settingsPage.loggingLevel.description.subtitle": "Increasing the log level increases disk space usage and can impact performance. We recommend only increasing the log level if you are having issues.", "renderer.components.settingsPage.loggingLevel.level.debug": "Debug (debug)", "renderer.components.settingsPage.loggingLevel.level.error": "Errors (error)", "renderer.components.settingsPage.loggingLevel.level.info": "Info (info)", @@ -254,18 +252,25 @@ "renderer.components.settingsPage.loggingLevel.level.warn": "Errors and Warnings (warn)", "renderer.components.settingsPage.minimizeToTray": "Leave app running in notification area when application window is closed", "renderer.components.settingsPage.minimizeToTray.description": "If enabled, the app stays running in the notification area after app window is closed.", - "renderer.components.settingsPage.saving.error": "Can't save your changes. Please try again.", + "renderer.components.settingsPage.notifications": "Notifications", + "renderer.components.settingsPage.saving": "Saving...", + "renderer.components.settingsPage.servers": "Servers", + "renderer.components.settingsPage.serverSetting.addAServer": "Add a server", + "renderer.components.settingsPage.serverSetting.noServers": "No servers added", + "renderer.components.settingsPage.serverSetting.noServers.description": "Add a server to connect to your team's communication hub", + "renderer.components.settingsPage.serverSetting.title": "Servers", "renderer.components.settingsPage.showUnreadBadge": "Show red badge on {taskbar} icon to indicate unread messages", "renderer.components.settingsPage.showUnreadBadge.description": "Regardless of this setting, mentions are always indicated with a red badge and item count on the {taskbar} icon.", + "renderer.components.settingsPage.showUnreadBadge.heading": "Unread Badge", + "renderer.components.settingsPage.spellChecker": "Spell Checker", + "renderer.components.settingsPage.spellCheckerSetting.language": "Spell Checker Languages", "renderer.components.settingsPage.startAppOnLogin": "Start app on login", "renderer.components.settingsPage.startAppOnLogin.description": "If enabled, the app starts automatically when you log in to your machine.", "renderer.components.settingsPage.trayIcon.color": "Icon color: ", "renderer.components.settingsPage.trayIcon.show": "Show icon in the notification area", - "renderer.components.settingsPage.trayIcon.show.darwin": "Show {appName} icon in the menu bar", "renderer.components.settingsPage.trayIcon.theme.dark": "Dark", "renderer.components.settingsPage.trayIcon.theme.light": "Light", "renderer.components.settingsPage.trayIcon.theme.systemDefault": "Use system default", - "renderer.components.settingsPage.updates": "Updates", "renderer.components.settingsPage.updates.automatic": "Automatically check for updates", "renderer.components.settingsPage.updates.automatic.description": "If enabled, updates to the Desktop App will download automatically and you will be notified when ready to install.", "renderer.components.settingsPage.updates.checkNow": "Check for Updates Now", diff --git a/src/app/serverViewState.ts b/src/app/serverViewState.ts index 8d562cce..e60f85eb 100644 --- a/src/app/serverViewState.ts +++ b/src/app/serverViewState.ts @@ -19,6 +19,10 @@ import { UPDATE_SHORTCUT_MENU, UPDATE_TAB_ORDER, VALIDATE_SERVER_URL, + GET_UNIQUE_SERVERS_WITH_PERMISSIONS, + ADD_SERVER, + EDIT_SERVER, + REMOVE_SERVER, } from 'common/communication'; import Config from 'common/config'; import {Logger} from 'common/log'; @@ -33,7 +37,7 @@ import ModalManager from 'main/views/modalManager'; import ViewManager from 'main/views/viewManager'; import MainWindow from 'main/windows/mainWindow'; -import type {Server} from 'types/config'; +import type {Server, UniqueServer} from 'types/config'; import type {Permissions, UniqueServerWithPermissions} from 'types/permissions'; import type {URLValidationResult} from 'types/server'; @@ -56,6 +60,11 @@ export class ServerViewState { ipcMain.handle(GET_LAST_ACTIVE, this.handleGetLastActive); ipcMain.handle(GET_ORDERED_TABS_FOR_SERVER, this.handleGetOrderedViewsForServer); ipcMain.on(UPDATE_TAB_ORDER, this.updateTabOrder); + + ipcMain.handle(GET_UNIQUE_SERVERS_WITH_PERMISSIONS, this.getUniqueServersWithPermissions); + ipcMain.on(ADD_SERVER, this.handleAddServer); + ipcMain.on(EDIT_SERVER, this.handleEditServer); + ipcMain.on(REMOVE_SERVER, this.handleRemoveServer); } init = () => { @@ -407,6 +416,51 @@ export class ServerViewState { const newView = filteredViews[nextIndex].view; ViewManager.showById(newView.id); }; + + private getUniqueServersWithPermissions = () => { + return ServerManager.getAllServers(). + map((server) => ({ + server: server.toUniqueServer(), + permissions: PermissionsManager.getForServer(server) ?? {}, + })); + }; + + private handleAddServer = (event: IpcMainEvent, server: Server) => { + log.debug('handleAddServer', server); + + ServerManager.addServer(server); + }; + + private handleEditServer = (event: IpcMainEvent, server: UniqueServer, permissions?: Permissions) => { + log.debug('handleEditServer', server, permissions); + + if (!server.id) { + return; + } + + if (!server.isPredefined) { + ServerManager.editServer(server.id, server); + } + if (permissions) { + const mattermostServer = ServerManager.getServer(server.id); + if (mattermostServer) { + PermissionsManager.setForServer(mattermostServer, permissions); + } + } + }; + + private handleRemoveServer = (event: IpcMainEvent, serverId: string) => { + log.debug('handleRemoveServer', serverId); + + const remainingServers = ServerManager.getOrderedServers().filter((orderedServer) => serverId !== orderedServer.id); + if (this.currentServerId === serverId && remainingServers.length) { + this.currentServerId = remainingServers[0].id; + } else if (!remainingServers.length) { + delete this.currentServerId; + } + + ServerManager.removeServer(serverId); + }; } const serverViewState = new ServerViewState(); diff --git a/src/common/communication.ts b/src/common/communication.ts index 9febeabd..8746fc35 100644 --- a/src/common/communication.ts +++ b/src/common/communication.ts @@ -189,3 +189,8 @@ export const IS_DEVELOPER_MODE_ENABLED = 'is-developer-mode-enabled'; export const METRICS_SEND = 'metrics-send'; export const METRICS_RECEIVE = 'metrics-receive'; export const METRICS_REQUEST = 'metrics-request'; + +export const GET_UNIQUE_SERVERS_WITH_PERMISSIONS = 'get-unique-servers-with-permissions'; +export const ADD_SERVER = 'add-server'; +export const EDIT_SERVER = 'edit-server'; +export const REMOVE_SERVER = 'remove-server'; diff --git a/src/main/app/intercom.ts b/src/main/app/intercom.ts index 41c4dfb1..87355616 100644 --- a/src/main/app/intercom.ts +++ b/src/main/app/intercom.ts @@ -38,16 +38,17 @@ function handleShowOnboardingScreens(showWelcomeScreen: boolean, showNewServerMo log.debug('handleShowOnboardingScreens', {showWelcomeScreen, showNewServerModal, mainWindowIsVisible}); if (showWelcomeScreen) { - if (ModalManager.isModalDisplayed()) { + const welcomeScreen = ModalManager.modalQueue.find((modal) => modal.key === 'welcomeScreen'); + if (welcomeScreen) { return; } handleWelcomeScreenModal(); if (process.env.NODE_ENV === 'test') { - const welcomeScreen = ModalManager.modalQueue.find((modal) => modal.key === 'welcomeScreen'); - if (welcomeScreen?.view.webContents.isLoading()) { - welcomeScreen?.view.webContents.once('did-finish-load', () => { + const welcomeScreenTest = ModalManager.modalQueue.find((modal) => modal.key === 'welcomeScreen'); + if (welcomeScreenTest?.view.webContents.isLoading()) { + welcomeScreenTest?.view.webContents.once('did-finish-load', () => { app.emit('e2e-app-loaded'); }); } else { diff --git a/src/main/notifications/index.ts b/src/main/notifications/index.ts index 47eb9dd6..d205d62f 100644 --- a/src/main/notifications/index.ts +++ b/src/main/notifications/index.ts @@ -266,7 +266,7 @@ function flashFrame(flash: boolean) { MainWindow.get()?.flashFrame(flash); } } - if (process.platform === 'darwin' && Config.notifications.bounceIcon) { + if (process.platform === 'darwin' && Config.notifications.bounceIcon && Config.notifications.bounceIconType) { app.dock.bounce(Config.notifications.bounceIconType); } } diff --git a/src/main/preload/internalAPI.js b/src/main/preload/internalAPI.js index 8f951973..590ed50b 100644 --- a/src/main/preload/internalAPI.js +++ b/src/main/preload/internalAPI.js @@ -93,6 +93,10 @@ import { IS_DEVELOPER_MODE_ENABLED, METRICS_REQUEST, METRICS_RECEIVE, + ADD_SERVER, + EDIT_SERVER, + REMOVE_SERVER, + GET_UNIQUE_SERVERS_WITH_PERMISSIONS, LOAD_INCOMPATIBLE_SERVER, OPEN_SERVER_UPGRADE_LINK, } from 'common/communication'; @@ -139,6 +143,10 @@ contextBridge.exposeInMainWorld('desktop', { getOrderedTabsForServer: (serverId) => ipcRenderer.invoke(GET_ORDERED_TABS_FOR_SERVER, serverId), onUpdateServers: (listener) => ipcRenderer.on(SERVERS_UPDATE, () => listener()), validateServerURL: (url, currentId) => ipcRenderer.invoke(VALIDATE_SERVER_URL, url, currentId), + getUniqueServersWithPermissions: () => ipcRenderer.invoke(GET_UNIQUE_SERVERS_WITH_PERMISSIONS), + addServer: (server) => ipcRenderer.send(ADD_SERVER, server), + editServer: (server, permissions) => ipcRenderer.send(EDIT_SERVER, server, permissions), + removeServer: (serverId) => ipcRenderer.send(REMOVE_SERVER, serverId), getConfiguration: () => ipcRenderer.invoke(GET_CONFIGURATION), getVersion: () => ipcRenderer.invoke(GET_APP_INFO), @@ -152,7 +160,10 @@ contextBridge.exposeInMainWorld('desktop', { getLanguageInformation: () => ipcRenderer.invoke(GET_LANGUAGE_INFORMATION), onSynchronizeConfig: (listener) => ipcRenderer.on('synchronize-config', () => listener()), - onReloadConfiguration: (listener) => ipcRenderer.on(RELOAD_CONFIGURATION, () => listener()), + onReloadConfiguration: (listener) => { + ipcRenderer.on(RELOAD_CONFIGURATION, () => listener()); + return () => ipcRenderer.off(RELOAD_CONFIGURATION, listener); + }, onDarkModeChange: (listener) => ipcRenderer.on(DARK_MODE_CHANGE, (_, darkMode) => listener(darkMode)), onLoadRetry: (listener) => ipcRenderer.on(LOAD_RETRY, (_, viewId, retry, err, loadUrl) => listener(viewId, retry, err, loadUrl)), onLoadSuccess: (listener) => ipcRenderer.on(LOAD_SUCCESS, (_, viewId) => listener(viewId)), @@ -248,18 +259,6 @@ contextBridge.exposeInMainWorld('desktop', { }, }); -// TODO: This is for modals only, should probably move this out for them -const createKeyDownListener = () => { - ipcRenderer.invoke(GET_MODAL_UNCLOSEABLE).then((uncloseable) => { - window.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && !uncloseable) { - ipcRenderer.send(MODAL_CANCEL); - } - }); - }); -}; -createKeyDownListener(); - ipcRenderer.on(METRICS_REQUEST, async (_, name) => { const memory = await process.getProcessMemoryInfo(); ipcRenderer.send(METRICS_RECEIVE, name, {cpu: process.getCPUUsage().percentCPUUsage, memory: memory.residentSet ?? memory.private}); diff --git a/src/renderer/components/AutoSaveIndicator.tsx b/src/renderer/components/AutoSaveIndicator.tsx deleted file mode 100644 index fc5f8934..00000000 --- a/src/renderer/components/AutoSaveIndicator.tsx +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2015-2016 Yuya Ochiai -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {Alert} from 'react-bootstrap'; -import type {IntlShape} from 'react-intl'; -import {useIntl} from 'react-intl'; - -const baseClassName = 'AutoSaveIndicator'; -const leaveClassName = `${baseClassName}-Leave`; - -export enum SavingState { - SAVING_STATE_SAVING = 'saving', - SAVING_STATE_SAVED = 'saved', - SAVING_STATE_ERROR = 'error', - SAVING_STATE_DONE = 'done', -} - -function getClassNameAndMessage(intl: IntlShape, savingState: SavingState, errorMessage?: React.ReactNode) { - switch (savingState) { - case SavingState.SAVING_STATE_SAVING: - return {className: baseClassName, message: intl.formatMessage({id: 'renderer.components.autoSaveIndicator.saving', defaultMessage: 'Saving...'})}; - case SavingState.SAVING_STATE_SAVED: - return {className: baseClassName, message: intl.formatMessage({id: 'renderer.components.autoSaveIndicator.saved', defaultMessage: 'Saved'})}; - case SavingState.SAVING_STATE_ERROR: - return {className: `${baseClassName}`, message: errorMessage}; - case SavingState.SAVING_STATE_DONE: - return {className: `${baseClassName} ${leaveClassName}`, message: intl.formatMessage({id: 'renderer.components.autoSaveIndicator.saved', defaultMessage: 'Saved'})}; - default: - return {className: `${baseClassName} ${leaveClassName}`, message: ''}; - } -} - -type Props = { - id?: string; - savingState: SavingState; - errorMessage?: React.ReactNode; -}; - -const AutoSaveIndicator: React.FC = (props: Props) => { - const intl = useIntl(); - const {savingState, errorMessage, ...rest} = props; - const {className, message} = getClassNameAndMessage(intl, savingState, errorMessage); - return ( - - {message} - - ); -}; - -export default AutoSaveIndicator; diff --git a/src/renderer/components/Images/server-small.tsx b/src/renderer/components/Images/server-small.tsx new file mode 100644 index 00000000..0ed56d73 --- /dev/null +++ b/src/renderer/components/Images/server-small.tsx @@ -0,0 +1,116 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +const ServerSmallImage = () => ( + + + + + + + + + + + + + + + + + +); + +export default ServerSmallImage; diff --git a/src/renderer/components/Modal.tsx b/src/renderer/components/Modal.tsx index 32370280..da7928ae 100644 --- a/src/renderer/components/Modal.tsx +++ b/src/renderer/components/Modal.tsx @@ -78,6 +78,29 @@ export const Modal: React.FC = ({ const [showState, setShowState] = useState(); const backdropRef = useRef(null); + const onClose = useCallback(async () => { + await onHide(); + onExited(); + }, [onExited]); + + useEffect(() => { + const escListener = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }; + async function createEscListener() { + const uncloseable = await window.desktop.modals.isModalUncloseable(); + if (!uncloseable) { + window.addEventListener('keydown', escListener); + } + } + createEscListener(); + return () => { + window.removeEventListener('keydown', escListener); + }; + }, [onClose]); + useEffect(() => { setShowState(show ?? true); }, [show]); @@ -91,11 +114,6 @@ export const Modal: React.FC = ({ }); }; - const onClose = useCallback(async () => { - await onHide(); - onExited(); - }, [onExited]); - const handleCancelClick = async (event: React.MouseEvent) => { event.preventDefault(); if (autoCloseOnCancelButton) { diff --git a/src/renderer/components/SettingsModal/SettingsModal.scss b/src/renderer/components/SettingsModal/SettingsModal.scss new file mode 100644 index 00000000..4caaaf9c --- /dev/null +++ b/src/renderer/components/SettingsModal/SettingsModal.scss @@ -0,0 +1,106 @@ +@use '../../css/css_variables'; + +.SettingsModal { + &.Modal_dialog { + width: 832px; + max-width: 832px; + } + + > .Modal_content { + border: var(--border-light); + display: flex; + flex-direction: column; + + > .Modal_body { + display: flex; + flex-direction: column; + flex: 1; + + > .Modal__body { + display: flex; + flex: 1; + min-height: 475px; + + .SettingsModal__sidebar { + flex: 0 0 auto; + width: 232px; + padding: 24px; + box-sizing: border-box; + background-color: rgba(var(--center-channel-color-rgb), 0.04); + + .SettingsModal__category { + border: none; + width: 100%; + padding: 8px 12px; + border-radius: var(--radius-s); + background: none; + color: rgba(var(--center-channel-color-rgb), 0.56); + display: flex; + align-items: center; + + & + .SettingsModal__category { + margin-top: 8px; + } + + &.selected { + background-color: rgba(var(--button-bg-rgb), 0.08); + color: var(--button-bg); + } + + &:hover:not(.selected) { + background-color: rgba(var(--center-channel-color-rgb), 0.04); + color: rgba(var(--center-channel-color-rgb), 0.8); + } + + > span { + font-weight: 600; + line-height: 14px; + font-size: 14px; + margin-left: 8px; + } + + > i { + font-size: 18px; + line-height: 18px; + + &::before { + margin: 0; + } + } + } + } + + .SettingsModal__content { + padding: 28px 32px; + border-left: var(--border-light); + width: 100%; + display: flex; + flex-direction: column; + + > div + div { + margin-top: 24px; + } + } + } + } + } + + .SettingsModal__saving { + padding: 8px 16px; + margin-right: 14px; + color: rgba(var(--center-channel-color-rgb), 0.56); + display: flex; + + > span { + font-size: 12px; + line-height: 16px; + font-weight: 600; + white-space: nowrap; + } + + > i { + font-size: 14.4px; + line-height: 14.4px; + } + } +} diff --git a/src/renderer/components/SettingsModal/components/CheckSetting.scss b/src/renderer/components/SettingsModal/components/CheckSetting.scss new file mode 100644 index 00000000..bbdbb090 --- /dev/null +++ b/src/renderer/components/SettingsModal/components/CheckSetting.scss @@ -0,0 +1,72 @@ +.CheckSetting { + .CheckSetting__heading { + font-size: 12px; + line-height: 16px; + font-weight: 600; + margin-bottom: 16px; + } + + .CheckSetting__content { + display: flex; + + .CheckSetting__checkbox { + margin-right: 10px; + background: none; + border: 1px solid rgba(var(--center-channel-color-rgb), 0.24); + border-radius: var(--radius-xs); + width: 16px; + height: 16px; + margin-top: 2px; + + input { + appearance: none; + position: absolute; + } + + > i { + display: block; + font-size: 0; + line-height: 16px; + position: relative; + right: -1px; + top: 2px; + width: 0; + height: 0; + border-radius: var(--radius-xs); + transition: all ease-in-out 0.175s; + background: none; + color: none; + + &::before { + margin: 0; + } + } + + &.checked { + border: 1px solid var(--button-bg); + + > i { + right: 7px; + top: -2px; + width: 16px; + height: 16px; + font-size: 14.4px; + background: var(--button-bg); + color: var(--button-color); + } + } + } + + .CheckSetting__label { + font-size: 14px; + line-height: 20px; + + .CheckSetting__sublabel { + margin-top: 4px; + font-size: 12px; + line-height: 16px; + color: rgba(var(--center-channel-color-rgb), 0.76) + } + } + } +} \ No newline at end of file diff --git a/src/renderer/components/SettingsModal/components/CheckSetting.tsx b/src/renderer/components/SettingsModal/components/CheckSetting.tsx new file mode 100644 index 00000000..650076c0 --- /dev/null +++ b/src/renderer/components/SettingsModal/components/CheckSetting.tsx @@ -0,0 +1,63 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import classNames from 'classnames'; +import React, {useState} from 'react'; + +import './CheckSetting.scss'; + +export default function CheckSetting({ + id, + onSave, + label, + heading, + subLabel, + ...props +}: { + id: string; + onSave: (key: string, value: boolean) => void; + label: React.ReactNode; + value: boolean; + heading?: React.ReactNode; + subLabel?: React.ReactNode; +}) { + const [value, setValue] = useState(props.value); + const save = () => { + onSave(id, !value); + setValue(!value); + }; + + return ( +
+ {heading &&
{heading}
} +
+ + +
+
+ ); +} diff --git a/src/renderer/components/SettingsModal/components/DownloadSetting.scss b/src/renderer/components/SettingsModal/components/DownloadSetting.scss new file mode 100644 index 00000000..4d4da1bf --- /dev/null +++ b/src/renderer/components/SettingsModal/components/DownloadSetting.scss @@ -0,0 +1,39 @@ +.DownloadSetting { + padding-bottom: 24px; + border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08); + + .DownloadSetting__content { + display: flex; + margin-top: 16px; + + .Input_container.disabled { + margin-top: 0; + + input { + height: 34px; + font-size: 14px; + line-height: 20px; + } + } + + button { + margin-left: 8px; + } + } + + .DownloadSetting__heading { + margin: 0; + } + + .DownloadSetting__label { + margin-top: 8px; + font-size: 12px; + line-height: 16px; + color: rgba(var(--center-channel-color-rgb), 0.64); + } +} + +div + .DownloadSetting { + padding-top: 24px; + border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08); +} \ No newline at end of file diff --git a/src/renderer/components/SettingsModal/components/DownloadSetting.tsx b/src/renderer/components/SettingsModal/components/DownloadSetting.tsx new file mode 100644 index 00000000..908883a8 --- /dev/null +++ b/src/renderer/components/SettingsModal/components/DownloadSetting.tsx @@ -0,0 +1,65 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useState} from 'react'; +import {FormattedMessage} from 'react-intl'; + +import './DownloadSetting.scss'; +import Input, {SIZE} from 'renderer/components/Input'; + +export default function DownloadSetting({ + id, + onSave, + ...props +}: { + id: string; + onSave: (key: string, value: string) => void; + label: React.ReactNode; + value: string; +}) { + const [value, setValue] = useState(props.value); + + const selectDownloadLocation = async () => { + const newDownloadLocation = await window.desktop.getDownloadLocation(props.value); + if (!newDownloadLocation) { + return; + } + + onSave(id, newDownloadLocation); + setValue(newDownloadLocation); + }; + + return ( +
+

+ +

+
+ +
+
+ + +
+
+ ); +} diff --git a/src/renderer/components/SettingsModal/components/NotificationSetting.tsx b/src/renderer/components/SettingsModal/components/NotificationSetting.tsx new file mode 100644 index 00000000..5e0f58b7 --- /dev/null +++ b/src/renderer/components/SettingsModal/components/NotificationSetting.tsx @@ -0,0 +1,104 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import type {Config} from 'types/config'; + +import CheckSetting from './CheckSetting'; +import RadioSetting from './RadioSetting'; + +export default function NotificationSetting({ + onSave, + value, +}: { + onSave: (key: 'notifications', value: Config['notifications']) => void; + value: Config['notifications']; +}) { + if (window.process.platform === 'darwin') { + return ( + onSave('notifications', { + ...value, + bounceIcon: Boolean(v), + bounceIconType: v, + })} + value={value.bounceIconType} + label={( + + )} + options={[ + { + value: 'informational', + label: ( + + ), + }, + { + value: 'critical', + label: ( + + ), + }, + { + value: '', + label: ( + + ), + }, + ]} + /> + ); + } + + return ( + onSave('notifications', {...value, [k]: v ? 2 : 0})} + value={value.flashWindow === 2} + label={( + + )} + subLabel={( + <> + + {window.process.platform === 'linux' && + <> +
+ + + + + + + } + + )} + /> + ); +} diff --git a/src/renderer/components/SettingsModal/components/RadioSetting.scss b/src/renderer/components/SettingsModal/components/RadioSetting.scss new file mode 100644 index 00000000..ec42ffe9 --- /dev/null +++ b/src/renderer/components/SettingsModal/components/RadioSetting.scss @@ -0,0 +1,81 @@ +.RadioSetting { + .RadioSetting__heading { + font-size: 12px; + line-height: 16px; + font-weight: 600; + margin-bottom: 16px; + } + + .RadioSetting__content { + display: flex; + flex-direction: column; + + /* Create a custom radio button */ + .RadioSetting__radio { + display: inline-block; + position: relative; + padding-left: 28px; + cursor: pointer; + font-size: 16px; + user-select: none; + border: none; + background: none; + color: var(--center-channel-color); + text-align: left; + + + .RadioSetting__radio { + margin-top: 12px; + } + + input[type="radio"] { + display: none; + cursor: pointer; + + + .RadioSetting__label::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 16px; + height: 16px; + border: 1px solid rgba(var(--center-channel-color-rgb), 0.24); + border-radius: 50%; + transition: border-color ease-in 0.175s; + } + + + .RadioSetting__label::after { + content: ""; + position: absolute; + left: 9px; + top: 50%; + transform: translateY(-50%); + width: 0; + height: 0; + border-radius: 50%; + background-color: var(--button-bg); + transition: width ease-in-out 0.175s, height ease-in-out 0.175s, left ease-in-out 0.175s; + } + + &:checked { + + .RadioSetting__label::before { + border-color: var(--button-bg); + } + + + .RadioSetting__label::after { + width: 8px; + height: 8px; + left: 5px; + } + } + } + + .RadioSetting__label { + display: block; + font-size: 14px; + line-height: 20px; + cursor: inherit; + } + } + } +} \ No newline at end of file diff --git a/src/renderer/components/SettingsModal/components/RadioSetting.tsx b/src/renderer/components/SettingsModal/components/RadioSetting.tsx new file mode 100644 index 00000000..210d379d --- /dev/null +++ b/src/renderer/components/SettingsModal/components/RadioSetting.tsx @@ -0,0 +1,62 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useState} from 'react'; + +import './RadioSetting.scss'; + +export default function RadioSetting({ + id, + onSave, + label, + options, + ...props +}: { + id: string; + onSave: (key: string, value: T) => void; + label: React.ReactNode; + value: T; + options: Array<{value: T; label: React.ReactNode}>; +}) { + const [value, setValue] = useState(props.value); + + const save = (value: T) => { + onSave(id, value); + setValue(value); + }; + + return ( +
+
{label}
+
+ {options.map((option, index) => ( + + ))} +
+
+ ); +} diff --git a/src/renderer/components/SettingsModal/components/SelectSetting.scss b/src/renderer/components/SettingsModal/components/SelectSetting.scss new file mode 100644 index 00000000..a259531e --- /dev/null +++ b/src/renderer/components/SettingsModal/components/SelectSetting.scss @@ -0,0 +1,93 @@ +.SelectSetting { + &.SelectSetting-bottomBorder { + border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08); + padding-bottom: 24px; + } + + .SelectSetting__select { + margin-top: 16px; + width: 50%; + + .SelectSetting__select__single-value { + color: rgba(var(--center-channel-color-rgb), 0.64); + } + + .SelectSetting__select__control { + background: var(--center-channel-bg); + border: 1px solid rgba(var(--center-channel-color-rgb), 0.16); + border-radius: var(--radius-s); + box-sizing: border-box; + + &:hover { + border-color: rgba(var(--center-channel-color-rgb), 0.48); + } + + &:focus-within { + border-color: var(--button-bg); + border-width: 1px; + } + } + + .SelectSetting__select__menu { + background: var(--center-channel-bg); + } + + .SelectSetting__select__menu-portal { + z-index: 10; + } + + .SelectSetting__select__option { + background: var(--center-channel-bg); + color: var(--center-channel-color); + + &:hover { + background-color: rgba(var(--center-channel-color-rgb), 0.16); + } + } + + .SelectSetting__select__indicator { + color: rgba(var(--center-channel-color-rgb), 0.64); + } + + .SelectSetting__select__indicator-separator { + display: none; + } + + .SelectSetting__select__multi-value { + border-radius: var(--radius-l); + border: none; + background-color: rgba(var(--center-channel-color-rgb), 0.08); + + .SelectSetting__select__multi-value__label { + font-size: 12px; + font-weight: 600; + line-height: 15px; + color: var(--center-channel-color); + padding: 4.5px; + padding-left: 10px; + } + + .SelectSetting__select__multi-value__remove { + padding: 0; + margin: 4.5px 10px 4.5px 1.5px; + border-radius: 50%; + background: rgba(var(--center-channel-color-rgb), 0.32); + + &:hover { + color: inherit; + }; + } + } + } + + .SelectSetting__heading { + margin: 0; + } + + .SelectSetting__label { + margin-top: 8px; + font-size: 12px; + line-height: 16px; + color: rgba(var(--center-channel-color-rgb), 0.64); + } +} \ No newline at end of file diff --git a/src/renderer/components/SettingsModal/components/SelectSetting.tsx b/src/renderer/components/SettingsModal/components/SelectSetting.tsx new file mode 100644 index 00000000..3956e5bc --- /dev/null +++ b/src/renderer/components/SettingsModal/components/SelectSetting.tsx @@ -0,0 +1,100 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import classNames from 'classnames'; +import React, {useState} from 'react'; +import ReactSelect from 'react-select'; +import type {ActionMeta, MultiValue, PropsValue, SingleValue} from 'react-select'; + +import './SelectSetting.scss'; + +type Option = {value: string; label: string}; + +type IsMulti = { + isMulti: true; + value: string[]; + onSave: (key: string, value: string[]) => void; +} | { + isMulti: false; + value: string; + onSave: (key: string, value: string) => void; +} +type Props = IsMulti & { + id: string; + label: React.ReactNode; + options: Option[]; + subLabel?: React.ReactNode; + placeholder?: React.ReactNode; + bottomBorder?: boolean; +}; + +function valueToOption(value: string | string[], options: Option[]): PropsValue