[MM-61717] Refresh Settings Modal without Bootstrap (#3337)
* [MM-61717] Refresh Settings Modal without Bootstrap * Fix i18n * Couple small bug fixes * E2E test updates * Fix linux tests * PR feedback * PR feedback * PR feedback * Fix the border opacity and height * PR feedback * PR feedback 2
This commit is contained in:
@@ -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();
|
||||
|
@@ -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';
|
||||
|
@@ -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 {
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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});
|
||||
|
@@ -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: Props) => {
|
||||
const intl = useIntl();
|
||||
const {savingState, errorMessage, ...rest} = props;
|
||||
const {className, message} = getClassNameAndMessage(intl, savingState, errorMessage);
|
||||
return (
|
||||
<Alert
|
||||
className={className}
|
||||
{...rest}
|
||||
variant={savingState === 'error' ? 'danger' : 'info'}
|
||||
>
|
||||
{message}
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoSaveIndicator;
|
116
src/renderer/components/Images/server-small.tsx
Normal file
116
src/renderer/components/Images/server-small.tsx
Normal file
@@ -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 = () => (
|
||||
<svg
|
||||
width='85'
|
||||
height='75'
|
||||
viewBox='0 0 85 75'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<rect
|
||||
x='0.5'
|
||||
y='0.441162'
|
||||
width='84'
|
||||
height='22.7294'
|
||||
rx='2.96471'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.12'
|
||||
/>
|
||||
<path
|
||||
opacity='0.48'
|
||||
d='M80.5472 4.39417H4.45312V19.2177H80.5472V4.39417Z'
|
||||
stroke='var(--center-channel-color)'
|
||||
strokeOpacity='0.75'
|
||||
strokeWidth='0.988235'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M21.2528 15.2646C22.8902 15.2646 24.2175 13.9373 24.2175 12.2999C24.2175 10.6626 22.8902 9.33521 21.2528 9.33521C19.6154 9.33521 18.2881 10.6626 18.2881 12.2999C18.2881 13.9373 19.6154 15.2646 21.2528 15.2646Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<path
|
||||
d='M30.1463 15.2646C31.7837 15.2646 33.1111 13.9373 33.1111 12.2999C33.1111 10.6626 31.7837 9.33521 30.1463 9.33521C28.509 9.33521 27.1816 10.6626 27.1816 12.2999C27.1816 13.9373 28.509 15.2646 30.1463 15.2646Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<path
|
||||
opacity='0.5'
|
||||
d='M12.3583 15.2646C13.9956 15.2646 15.323 13.9373 15.323 12.2999C15.323 10.6626 13.9956 9.33521 12.3583 9.33521C10.7209 9.33521 9.39355 10.6626 9.39355 12.2999C9.39355 13.9373 10.7209 15.2646 12.3583 15.2646Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<rect
|
||||
x='0.5'
|
||||
y='26.1353'
|
||||
width='84'
|
||||
height='22.7294'
|
||||
rx='2.96471'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.12'
|
||||
/>
|
||||
<path
|
||||
opacity='0.48'
|
||||
d='M80.5472 30.0883H4.45312V44.9118H80.5472V30.0883Z'
|
||||
stroke='var(--center-channel-color)'
|
||||
strokeOpacity='0.75'
|
||||
strokeWidth='0.988235'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M21.2528 40.9588C22.8902 40.9588 24.2175 39.6315 24.2175 37.9941C24.2175 36.3568 22.8902 35.0294 21.2528 35.0294C19.6154 35.0294 18.2881 36.3568 18.2881 37.9941C18.2881 39.6315 19.6154 40.9588 21.2528 40.9588Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<path
|
||||
d='M30.1463 40.9588C31.7837 40.9588 33.1111 39.6315 33.1111 37.9941C33.1111 36.3568 31.7837 35.0294 30.1463 35.0294C28.509 35.0294 27.1816 36.3568 27.1816 37.9941C27.1816 39.6315 28.509 40.9588 30.1463 40.9588Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<path
|
||||
opacity='0.5'
|
||||
d='M12.3583 40.9588C13.9956 40.9588 15.323 39.6315 15.323 37.9941C15.323 36.3568 13.9956 35.0294 12.3583 35.0294C10.7209 35.0294 9.39355 36.3568 9.39355 37.9941C9.39355 39.6315 10.7209 40.9588 12.3583 40.9588Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<rect
|
||||
x='0.5'
|
||||
y='51.8295'
|
||||
width='84'
|
||||
height='22.7294'
|
||||
rx='2.96471'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.12'
|
||||
/>
|
||||
<path
|
||||
opacity='0.48'
|
||||
d='M80.5472 55.7823H4.45312V70.6059H80.5472V55.7823Z'
|
||||
stroke='var(--center-channel-color)'
|
||||
strokeOpacity='0.75'
|
||||
strokeWidth='0.988235'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M21.2528 66.6529C22.8902 66.6529 24.2175 65.3256 24.2175 63.6882C24.2175 62.0509 22.8902 60.7235 21.2528 60.7235C19.6154 60.7235 18.2881 62.0509 18.2881 63.6882C18.2881 65.3256 19.6154 66.6529 21.2528 66.6529Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<path
|
||||
d='M30.1463 66.6529C31.7837 66.6529 33.1111 65.3256 33.1111 63.6882C33.1111 62.0509 31.7837 60.7235 30.1463 60.7235C28.509 60.7235 27.1816 62.0509 27.1816 63.6882C27.1816 65.3256 28.509 66.6529 30.1463 66.6529Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<path
|
||||
opacity='0.5'
|
||||
d='M12.3583 66.6529C13.9956 66.6529 15.323 65.3256 15.323 63.6882C15.323 62.0509 13.9956 60.7235 12.3583 60.7235C10.7209 60.7235 9.39355 62.0509 9.39355 63.6882C9.39355 65.3256 10.7209 66.6529 12.3583 66.6529Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default ServerSmallImage;
|
@@ -78,6 +78,29 @@ export const Modal: React.FC<Props> = ({
|
||||
const [showState, setShowState] = useState<boolean>();
|
||||
const backdropRef = useRef<HTMLDivElement>(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<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const onClose = useCallback(async () => {
|
||||
await onHide();
|
||||
onExited();
|
||||
}, [onExited]);
|
||||
|
||||
const handleCancelClick = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
event.preventDefault();
|
||||
if (autoCloseOnCancelButton) {
|
||||
|
106
src/renderer/components/SettingsModal/SettingsModal.scss
Normal file
106
src/renderer/components/SettingsModal/SettingsModal.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 (
|
||||
<div
|
||||
id={`CheckSetting_${id}`}
|
||||
className='CheckSetting'
|
||||
>
|
||||
{heading && <div className='CheckSetting__heading'>{heading}</div>}
|
||||
<div className='CheckSetting__content'>
|
||||
<button
|
||||
className={classNames('CheckSetting__checkbox', {checked: value})}
|
||||
onClick={save}
|
||||
role='checkbox'
|
||||
aria-checked={value}
|
||||
aria-labelledby={`checkSetting-${id}`}
|
||||
>
|
||||
<input
|
||||
id={`checkSetting-${id}`}
|
||||
defaultChecked={value}
|
||||
type='checkbox'
|
||||
tabIndex={-1}
|
||||
disabled={true}
|
||||
/>
|
||||
<i className='icon-check'/>
|
||||
</button>
|
||||
<label
|
||||
htmlFor={`checkSetting-${id}`}
|
||||
className='CheckSetting__label'
|
||||
>
|
||||
{label}
|
||||
{subLabel && <div className='CheckSetting__sublabel'>{subLabel}</div>}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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 (
|
||||
<div className='DownloadSetting'>
|
||||
<h3 className='DownloadSetting__heading'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.downloadLocation'
|
||||
defaultMessage='Download Location'
|
||||
/>
|
||||
</h3>
|
||||
<div className='DownloadSetting__label'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.downloadLocation.description'
|
||||
defaultMessage='Specify the folder where files will download.'
|
||||
/>
|
||||
</div>
|
||||
<div className='DownloadSetting__content'>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={value}
|
||||
inputSize={SIZE.MEDIUM}
|
||||
/>
|
||||
<button
|
||||
className='DownloadSetting__changeButton btn btn-tertiary'
|
||||
id='saveDownloadLocation'
|
||||
onClick={selectDownloadLocation}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='label.change'
|
||||
defaultMessage='Change'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -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 (
|
||||
<RadioSetting
|
||||
id='notifications.bounceIconType'
|
||||
onSave={(k, v) => onSave('notifications', {
|
||||
...value,
|
||||
bounceIcon: Boolean(v),
|
||||
bounceIconType: v,
|
||||
})}
|
||||
value={value.bounceIconType}
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.bounceIconType'
|
||||
defaultMessage='Bounce the Dock icon...'
|
||||
/>
|
||||
)}
|
||||
options={[
|
||||
{
|
||||
value: 'informational',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.bounceIcon.once'
|
||||
defaultMessage='Once'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'critical',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.bounceIcon.untilOpenApp'
|
||||
defaultMessage='Until I open the app'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.bounceIcon.never'
|
||||
defaultMessage='Never'
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CheckSetting
|
||||
id='flashWindow'
|
||||
onSave={(k, v) => onSave('notifications', {...value, [k]: v ? 2 : 0})}
|
||||
value={value.flashWindow === 2}
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.flashWindow'
|
||||
defaultMessage='Flash taskbar icon when a new message is received'
|
||||
/>
|
||||
)}
|
||||
subLabel={(
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.flashWindow.description'
|
||||
defaultMessage='If enabled, the taskbar icon will flash for a few seconds when a new message is received.'
|
||||
/>
|
||||
{window.process.platform === 'linux' &&
|
||||
<>
|
||||
<br/>
|
||||
<em>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.flashWindow.description.note'
|
||||
defaultMessage='NOTE: '
|
||||
/>
|
||||
</strong>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.flashWindow.description.linuxFunctionality'
|
||||
defaultMessage='This functionality may not work with all Linux window managers.'
|
||||
/>
|
||||
</em>
|
||||
</>}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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<T extends string>({
|
||||
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 (
|
||||
<div className='RadioSetting'>
|
||||
<div className='RadioSetting__heading'>{label}</div>
|
||||
<div
|
||||
className='RadioSetting__content'
|
||||
role='radiogroup'
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<button
|
||||
id={`RadioSetting_${id}_${option.value}`}
|
||||
className='RadioSetting__radio'
|
||||
key={`${index}`}
|
||||
onClick={() => save(option.value)}
|
||||
role='radio'
|
||||
aria-checked={value === option.value}
|
||||
>
|
||||
<input
|
||||
type='radio'
|
||||
value={option.value}
|
||||
name={id}
|
||||
checked={value === option.value}
|
||||
readOnly={true}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`RadioSetting_${id}_${option.value}`}
|
||||
className='RadioSetting__label'
|
||||
>
|
||||
{option.label}
|
||||
</label>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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<Option> {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => options.find((o) => o.value === v)!);
|
||||
}
|
||||
|
||||
return options.find((o) => o.value === value)!;
|
||||
}
|
||||
|
||||
export default function SelectSetting({
|
||||
id,
|
||||
onSave,
|
||||
label,
|
||||
options,
|
||||
isMulti,
|
||||
subLabel,
|
||||
bottomBorder,
|
||||
value: propValue,
|
||||
placeholder,
|
||||
}: Props) {
|
||||
const [value, setValue] = useState<PropsValue<Option>>(valueToOption(propValue, options));
|
||||
|
||||
const save = (newValue: PropsValue<Option>, actionMeta: ActionMeta<Option>) => {
|
||||
if (isMulti) {
|
||||
let values = [...(value as MultiValue<Option>).map((v) => v.value)];
|
||||
switch (actionMeta.action) {
|
||||
case 'select-option':
|
||||
values = [...(newValue as MultiValue<Option>).map((v) => v.value)];
|
||||
break;
|
||||
case 'remove-value':
|
||||
values = values.filter((v) => v !== actionMeta.removedValue.value);
|
||||
break;
|
||||
case 'clear':
|
||||
values = [];
|
||||
break;
|
||||
}
|
||||
|
||||
onSave(id, values);
|
||||
} else {
|
||||
const singleValue = newValue as SingleValue<Option>;
|
||||
if (!singleValue) {
|
||||
return;
|
||||
}
|
||||
onSave(id, singleValue.value);
|
||||
}
|
||||
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames('SelectSetting', {'SelectSetting-bottomBorder': bottomBorder})}>
|
||||
<h3 className='SelectSetting__heading'>
|
||||
{label}
|
||||
</h3>
|
||||
{subLabel && <div className='SelectSetting__label'>
|
||||
{subLabel}
|
||||
</div>}
|
||||
<ReactSelect
|
||||
inputId={`selectSetting_${id}`}
|
||||
className='SelectSetting__select'
|
||||
classNamePrefix='SelectSetting__select'
|
||||
options={options}
|
||||
onChange={save}
|
||||
isMulti={isMulti}
|
||||
value={value}
|
||||
menuPosition='fixed'
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
.ServerSetting {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
.ServerSetting__heading {
|
||||
display: flex;
|
||||
padding-bottom: 20px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--center-channel-color-8, rgba(63, 67, 80, 0.08));
|
||||
|
||||
h3 {
|
||||
margin-right: auto;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ServerSetting__server {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--center-channel-color-8, rgba(63, 67, 80, 0.08));
|
||||
|
||||
i.icon-server-variant {
|
||||
font-size: 18px;
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ServerSetting__serverName {
|
||||
margin-left: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.ServerSetting__serverUrl {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.btn.btn-tertiary.btn-danger:not(:hover) {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.ServerSetting__noServers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
|
||||
span {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64)
|
||||
}
|
||||
|
||||
.ServerSetting__noServersTitle {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 600;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.ServerSetting__noServersDescription {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
max-width: 283px;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import ServerSmallImage from 'renderer/components/Images/server-small';
|
||||
import NewServerModal from 'renderer/components/NewServerModal';
|
||||
import RemoveServerModal from 'renderer/components/RemoveServerModal';
|
||||
|
||||
import type {Server, UniqueServer} from 'types/config';
|
||||
import type {Permissions, UniqueServerWithPermissions} from 'types/permissions';
|
||||
|
||||
import './ServerSetting.scss';
|
||||
|
||||
enum Modal {
|
||||
ADD = 1,
|
||||
EDIT,
|
||||
REMOVE,
|
||||
}
|
||||
|
||||
export default function ServerSetting() {
|
||||
const [servers, setServers] = useState<UniqueServerWithPermissions[]>([]);
|
||||
const [currentServer, setCurrentServer] = useState<UniqueServerWithPermissions>();
|
||||
const [modal, setModal] = useState<Modal>();
|
||||
|
||||
const reloadServers = useCallback(() => {
|
||||
window.desktop.getUniqueServersWithPermissions().then(setServers);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const off = window.desktop.onReloadConfiguration(reloadServers);
|
||||
reloadServers();
|
||||
|
||||
return () => off();
|
||||
}, []);
|
||||
|
||||
const closeModal = () => {
|
||||
setCurrentServer(undefined);
|
||||
setModal(undefined);
|
||||
};
|
||||
|
||||
const addServer = (server: Server) => {
|
||||
window.desktop.addServer(server);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const editServer = (server: UniqueServer, permissions?: Permissions) => {
|
||||
window.desktop.editServer(server, permissions);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const removeServer = () => {
|
||||
if (currentServer?.server.id) {
|
||||
window.desktop.removeServer(currentServer.server.id);
|
||||
}
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const showAddServerModal = () => {
|
||||
setModal(Modal.ADD);
|
||||
};
|
||||
|
||||
const showEditServerModal = (server: UniqueServerWithPermissions) => () => {
|
||||
setCurrentServer(server);
|
||||
setModal(Modal.EDIT);
|
||||
};
|
||||
|
||||
const showRemoveServerModal = (server: UniqueServerWithPermissions) => () => {
|
||||
setCurrentServer(server);
|
||||
setModal(Modal.REMOVE);
|
||||
};
|
||||
|
||||
let openModal;
|
||||
switch (modal) {
|
||||
case Modal.ADD:
|
||||
openModal = (
|
||||
<NewServerModal
|
||||
onClose={closeModal}
|
||||
onSave={addServer}
|
||||
show={true}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case Modal.EDIT:
|
||||
openModal = (
|
||||
<NewServerModal
|
||||
onClose={closeModal}
|
||||
onSave={editServer}
|
||||
editMode={true}
|
||||
show={true}
|
||||
server={currentServer?.server}
|
||||
permissions={currentServer?.permissions}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case Modal.REMOVE:
|
||||
openModal = (
|
||||
<RemoveServerModal
|
||||
show={true}
|
||||
onHide={closeModal}
|
||||
onCancel={closeModal}
|
||||
onAccept={removeServer}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='ServerSetting'>
|
||||
<div className='ServerSetting__heading'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.serverSetting.title'
|
||||
defaultMessage='Servers'
|
||||
/>
|
||||
</h3>
|
||||
<button
|
||||
onClick={showAddServerModal}
|
||||
className='ServerSetting__addServer btn btn-sm btn-tertiary'
|
||||
>
|
||||
<i className='icon icon-plus'/>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.serverSetting.addAServer'
|
||||
defaultMessage='Add a server'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{servers.length === 0 && (
|
||||
<div className='ServerSetting__noServers'>
|
||||
<ServerSmallImage/>
|
||||
<div className='ServerSetting__noServersTitle'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.serverSetting.noServers'
|
||||
defaultMessage='No servers added'
|
||||
/>
|
||||
</div>
|
||||
<div className='ServerSetting__noServersDescription'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.serverSetting.noServers.description'
|
||||
defaultMessage="Add a server to connect to your team's communication hub"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='ServerSetting__serverList'>
|
||||
{(servers.map((server, index) => (
|
||||
<div
|
||||
key={`${index}`}
|
||||
className='ServerSetting__server'
|
||||
>
|
||||
<i className='icon icon-server-variant'/>
|
||||
<div className='ServerSetting__serverName'>
|
||||
{server.server.name}
|
||||
</div>
|
||||
<div className='ServerSetting__serverUrl'>
|
||||
{server.server.url}
|
||||
</div>
|
||||
<button
|
||||
onClick={showEditServerModal(server)}
|
||||
className='ServerSetting__editServer btn btn-icon btn-sm'
|
||||
>
|
||||
<i className='icon icon-pencil-outline'/>
|
||||
</button>
|
||||
<button
|
||||
onClick={showRemoveServerModal(server)}
|
||||
className='ServerSetting__removeServer btn btn-icon btn-sm btn-tertiary btn-transparent btn-danger'
|
||||
>
|
||||
<i className='icon icon-trash-can-outline'/>
|
||||
</button>
|
||||
</div>
|
||||
)))}
|
||||
</div>
|
||||
{openModal}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
.SpellCheckerSetting {
|
||||
> .SelectSetting {
|
||||
margin-top: 24px;
|
||||
|
||||
.SelectSetting__heading {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.SelectSetting__select {
|
||||
width: 100%;
|
||||
}
|
||||
};
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.SpellCheckerSetting__alternative {
|
||||
.SpellCheckerSetting__alternative__content {
|
||||
display: flex;
|
||||
margin-top: 16px;
|
||||
|
||||
.Input_container {
|
||||
margin-top: 0;
|
||||
|
||||
input {
|
||||
height: 34px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.SpellCheckerSetting__alternative__heading {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.SpellCheckerSetting__alternative__label {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div + .SpellCheckerSetting {
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import Input, {SIZE} from 'renderer/components/Input';
|
||||
|
||||
import CheckSetting from './CheckSetting';
|
||||
import SelectSetting from './SelectSetting';
|
||||
|
||||
import './SpellCheckerSetting.scss';
|
||||
|
||||
type Option = {value: string; label: string};
|
||||
|
||||
export default function SpellCheckerSetting({
|
||||
id,
|
||||
onSave,
|
||||
label,
|
||||
options,
|
||||
subLabel,
|
||||
heading,
|
||||
value: propValue,
|
||||
}: {
|
||||
id: string;
|
||||
onSave: (key: string, value: string | boolean | string[]) => void;
|
||||
label: React.ReactNode;
|
||||
options: Option[];
|
||||
subLabel?: React.ReactNode;
|
||||
heading?: React.ReactNode;
|
||||
value: boolean;
|
||||
}) {
|
||||
const [spellCheckerLocales, setSpellCheckerLocales] = useState<string[]>();
|
||||
|
||||
const [spellCheckerURL, setSpellCheckerURL] = useState<string>();
|
||||
const [editingURL, setEditingURL] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Unfortunately we need to sidestep the props for this one as it is a very special case
|
||||
window.desktop.getLocalConfiguration().then((config) => {
|
||||
setSpellCheckerLocales(config.spellCheckerLocales);
|
||||
setSpellCheckerURL(config.spellCheckerURL);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const saveSpellCheckerLocales = (key: string, newValue: string[]) => {
|
||||
onSave('spellCheckerLocales', newValue);
|
||||
setSpellCheckerLocales(newValue);
|
||||
};
|
||||
|
||||
const editURL = () => {
|
||||
if (editingURL) {
|
||||
onSave('spellCheckerURL', spellCheckerURL ?? '');
|
||||
}
|
||||
setEditingURL(!editingURL);
|
||||
};
|
||||
|
||||
if (!spellCheckerLocales) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='SpellCheckerSetting'>
|
||||
<CheckSetting
|
||||
id={id}
|
||||
onSave={onSave}
|
||||
label={label}
|
||||
subLabel={subLabel}
|
||||
value={propValue}
|
||||
heading={heading}
|
||||
/>
|
||||
{propValue &&
|
||||
<SelectSetting
|
||||
id='spellCheckerLocales'
|
||||
onSave={saveSpellCheckerLocales}
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.spellCheckerSetting.language'
|
||||
defaultMessage='Spell Checker Languages'
|
||||
/>
|
||||
)}
|
||||
options={options}
|
||||
value={spellCheckerLocales}
|
||||
isMulti={true}
|
||||
placeholder={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.preferredLanguages'
|
||||
defaultMessage='Select preferred language(s)'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{propValue &&
|
||||
<div className='SpellCheckerSetting__alternative'>
|
||||
<h4 className='SpellCheckerSetting__alternative__heading'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.editSpellcheckUrl'
|
||||
defaultMessage='Use an alternative dictionary URL'
|
||||
/>
|
||||
</h4>
|
||||
<div className='SpellCheckerSetting__alternative__label'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.specifyURL'
|
||||
defaultMessage='Specify the url where dictionary definitions can be retrieved'
|
||||
/>
|
||||
</div>
|
||||
<div className='SpellCheckerSetting__alternative__content'>
|
||||
<Input
|
||||
disabled={!editingURL}
|
||||
value={spellCheckerURL}
|
||||
inputSize={SIZE.MEDIUM}
|
||||
onChange={(e) => setSpellCheckerURL(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className={classNames('DownloadSetting__changeButton btn', {
|
||||
'btn-primary': editingURL,
|
||||
'btn-tertiary': !editingURL,
|
||||
})}
|
||||
id='saveDownloadLocation'
|
||||
onClick={editURL}
|
||||
>
|
||||
{editingURL &&
|
||||
<FormattedMessage
|
||||
id='label.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
}
|
||||
{!editingURL &&
|
||||
<FormattedMessage
|
||||
id='label.change'
|
||||
defaultMessage='Change'
|
||||
/>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
.UpdatesSetting__button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.UpdatesSetting__subLabel > span {
|
||||
display: block;
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
// 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 CheckSetting from './CheckSetting';
|
||||
|
||||
import './UpdatesSetting.scss';
|
||||
|
||||
export default function UpdatesSetting({
|
||||
id,
|
||||
onSave,
|
||||
value,
|
||||
}: {
|
||||
id: string;
|
||||
onSave: (key: string, value: boolean) => void;
|
||||
value: boolean;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<CheckSetting
|
||||
id={id}
|
||||
onSave={onSave}
|
||||
value={value}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.updates.automatic'
|
||||
defaultMessage='Automatically check for updates'
|
||||
/>
|
||||
}
|
||||
subLabel={
|
||||
<div className='UpdatesSetting__subLabel'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.updates.automatic.description'
|
||||
defaultMessage='If enabled, updates to the Desktop App will download automatically and you will be notified when ready to install.'
|
||||
/>
|
||||
<button
|
||||
className='UpdatesSetting__button btn btn-primary'
|
||||
id='checkForUpdatesNow'
|
||||
onClick={window.desktop.checkForUpdates}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.updates.checkNow'
|
||||
defaultMessage='Check for Updates Now'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
449
src/renderer/components/SettingsModal/definition.tsx
Normal file
449
src/renderer/components/SettingsModal/definition.tsx
Normal file
@@ -0,0 +1,449 @@
|
||||
// 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 {IntlShape} from 'react-intl';
|
||||
|
||||
import {localeTranslations} from 'common/utils/constants';
|
||||
|
||||
import type {SettingsDefinition} from 'types/settings';
|
||||
|
||||
import CheckSetting from './components/CheckSetting';
|
||||
import DownloadSetting from './components/DownloadSetting';
|
||||
import NotificationSetting from './components/NotificationSetting';
|
||||
import RadioSetting from './components/RadioSetting';
|
||||
import SelectSetting from './components/SelectSetting';
|
||||
import ServerSetting from './components/ServerSetting';
|
||||
import SpellCheckerSetting from './components/SpellCheckerSetting';
|
||||
import UpdatesSetting from './components/UpdatesSetting';
|
||||
|
||||
const getLanguages = async (func: () => Promise<string[]>) => {
|
||||
return (await func()).filter((language) => localeTranslations[language]).
|
||||
map((language) => ({label: localeTranslations[language], value: language})).
|
||||
sort((a, b) => a.label.localeCompare(b.label));
|
||||
};
|
||||
|
||||
const definition: (intl: IntlShape) => Promise<SettingsDefinition> = async (intl: IntlShape) => {
|
||||
return {
|
||||
general: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.general'
|
||||
defaultMessage='General'
|
||||
/>
|
||||
),
|
||||
icon: 'settings-outline',
|
||||
settings: [
|
||||
{
|
||||
id: 'autoCheckForUpdates',
|
||||
component: UpdatesSetting,
|
||||
condition: (await window.desktop.getLocalConfiguration()).canUpgrade,
|
||||
},
|
||||
{
|
||||
id: 'downloadLocation',
|
||||
component: DownloadSetting,
|
||||
},
|
||||
{
|
||||
id: 'autostart',
|
||||
component: CheckSetting,
|
||||
condition: window.process.platform === 'win32' || window.process.platform === 'linux',
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.startAppOnLogin'
|
||||
defaultMessage='Start app on login'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.startAppOnLogin.description'
|
||||
defaultMessage='If enabled, the app starts automatically when you log in to your machine.'
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'hideOnStart',
|
||||
component: CheckSetting,
|
||||
condition: window.process.platform === 'win32' || window.process.platform === 'linux',
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.launchAppMinimized'
|
||||
defaultMessage='Launch app minimized'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.launchAppMinimized.description'
|
||||
defaultMessage='If enabled, the app will start in system tray, and will not show the window on launch.'
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'showTrayIcon',
|
||||
component: CheckSetting,
|
||||
condition: window.process.platform === 'darwin' || window.process.platform === 'linux',
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.show'
|
||||
defaultMessage='Show icon in the notification area'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'trayIconTheme',
|
||||
component: RadioSetting,
|
||||
condition: (window.process.platform === 'linux' || window.process.platform === 'win32') && (await window.desktop.getLocalConfiguration()).showTrayIcon,
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.color'
|
||||
defaultMessage='Icon color'
|
||||
/>
|
||||
),
|
||||
options: [
|
||||
{
|
||||
value: 'use_system',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.theme.systemDefault'
|
||||
defaultMessage='Use system default'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'light',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.theme.light'
|
||||
defaultMessage='Light'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.theme.dark'
|
||||
defaultMessage='Dark'
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'minimizeToTray',
|
||||
component: CheckSetting,
|
||||
condition: window.process.platform === 'linux' || window.process.platform === 'win32',
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.minimizeToTray'
|
||||
defaultMessage='Leave app running in notification area when application window is closed'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.minimizeToTray.description'
|
||||
defaultMessage='If enabled, the app stays running in the notification area after app window is closed.'
|
||||
/>
|
||||
<br/>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'startInFullscreen',
|
||||
component: CheckSetting,
|
||||
condition: window.process.platform !== 'linux',
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.fullscreen'
|
||||
defaultMessage='Open app in full screen'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.fullscreen.description'
|
||||
defaultMessage='If enabled, the {appName} application will always open in full screen'
|
||||
values={{appName: (await window.desktop.getVersion()).name}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
notifications: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.notifications'
|
||||
defaultMessage='Notifications'
|
||||
/>
|
||||
),
|
||||
icon: 'bell-outline',
|
||||
settings: [
|
||||
{
|
||||
id: 'showUnreadBadge',
|
||||
component: CheckSetting,
|
||||
condition: window.process.platform === 'darwin' || window.process.platform === 'win32',
|
||||
props: {
|
||||
heading: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.showUnreadBadge.heading'
|
||||
defaultMessage='Unread Badge'
|
||||
/>
|
||||
),
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.showUnreadBadge'
|
||||
defaultMessage='Show red badge on {taskbar} icon to indicate unread messages'
|
||||
values={{taskbar: window.process.platform === 'win32' ? 'taskbar' : 'Dock'}}
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.showUnreadBadge.description'
|
||||
defaultMessage='Regardless of this setting, mentions are always indicated with a red badge and item count on the {taskbar} icon.'
|
||||
values={{taskbar: window.process.platform === 'win32' ? 'taskbar' : 'Dock'}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'notifications',
|
||||
component: NotificationSetting,
|
||||
},
|
||||
],
|
||||
},
|
||||
language: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.language'
|
||||
defaultMessage='Language'
|
||||
/>
|
||||
),
|
||||
icon: 'globe',
|
||||
settings: [
|
||||
{
|
||||
id: 'appLanguage',
|
||||
component: SelectSetting,
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.appLanguage'
|
||||
defaultMessage='App Language'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.appLanguage.description'
|
||||
defaultMessage='The language that the Desktop App will use for menu items and popups. Still in beta, some languages will be missing translation strings.'
|
||||
/>
|
||||
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
</>
|
||||
),
|
||||
placeholder: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.appLanguage.placeholder'
|
||||
defaultMessage='Use system default'
|
||||
/>
|
||||
),
|
||||
options: await getLanguages(window.desktop.getAvailableLanguages),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'useSpellChecker',
|
||||
component: SpellCheckerSetting,
|
||||
props: {
|
||||
heading: (
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.spellChecker'
|
||||
defaultMessage='Spell Checker'
|
||||
/>
|
||||
</h3>
|
||||
),
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling'
|
||||
defaultMessage='Check spelling'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.description'
|
||||
defaultMessage='Highlight misspelled words in your messages based on your system language or language preference.'
|
||||
/>
|
||||
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
</>
|
||||
),
|
||||
options: await getLanguages(window.desktop.getAvailableSpellCheckerLanguages),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
servers: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.servers'
|
||||
defaultMessage='Servers'
|
||||
/>
|
||||
),
|
||||
icon: 'server-variant',
|
||||
settings: [
|
||||
{
|
||||
id: 'teams',
|
||||
component: ServerSetting,
|
||||
},
|
||||
],
|
||||
},
|
||||
advanced: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.advanced'
|
||||
defaultMessage='Advanced'
|
||||
/>
|
||||
),
|
||||
icon: 'tune',
|
||||
settings: [
|
||||
{
|
||||
id: 'logLevel',
|
||||
component: SelectSetting,
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.loggingLevel'
|
||||
defaultMessage='Logging level'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.loggingLevel.description'
|
||||
defaultMessage='Logging is helpful for developers and support to isolate issues you may be encountering with the desktop app.'
|
||||
/>
|
||||
),
|
||||
bottomBorder: true,
|
||||
options: [
|
||||
{
|
||||
value: 'error',
|
||||
label: intl.formatMessage({
|
||||
id: 'renderer.components.settingsPage.loggingLevel.level.error',
|
||||
defaultMessage: 'Errors (error)',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'warn',
|
||||
label: intl.formatMessage({
|
||||
id: 'renderer.components.settingsPage.loggingLevel.level.warn',
|
||||
defaultMessage: 'Errors and Warnings (warn)',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'info',
|
||||
label: intl.formatMessage({
|
||||
id: 'renderer.components.settingsPage.loggingLevel.level.info',
|
||||
defaultMessage: 'Info (info)',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'verbose',
|
||||
label: intl.formatMessage({
|
||||
id: 'renderer.components.settingsPage.loggingLevel.level.verbose',
|
||||
defaultMessage: 'Verbose (verbose)',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'debug',
|
||||
label: intl.formatMessage({
|
||||
id: 'renderer.components.settingsPage.loggingLevel.level.debug',
|
||||
defaultMessage: 'Debug (debug)',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'silly',
|
||||
label: intl.formatMessage({
|
||||
id: 'renderer.components.settingsPage.loggingLevel.level.silly',
|
||||
defaultMessage: 'Finest (silly)',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'enableMetrics',
|
||||
component: CheckSetting,
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.enableMetrics'
|
||||
defaultMessage='Send anonymous usage data to your configured servers'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.enableMetrics.description'
|
||||
defaultMessage='Sends usage data about the application and its performance to your configured servers that accept it.'
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'enableHardwareAcceleration',
|
||||
component: CheckSetting,
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.enableHardwareAcceleration'
|
||||
defaultMessage='Use GPU hardware acceleration'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.enableHardwareAcceleration.description'
|
||||
defaultMessage='If enabled, {appName} UI is rendered more efficiently but can lead to decreased stability for some systems.'
|
||||
values={{appName: (await window.desktop.getVersion()).name}}
|
||||
/>
|
||||
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default definition;
|
190
src/renderer/components/SettingsModal/index.tsx
Normal file
190
src/renderer/components/SettingsModal/index.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import createCache from '@emotion/cache';
|
||||
import type {EmotionCache} from '@emotion/react';
|
||||
import {CacheProvider} from '@emotion/react';
|
||||
import classNames from 'classnames';
|
||||
import React, {useEffect, useRef, useState, useCallback} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {Modal} from 'renderer/components/Modal';
|
||||
|
||||
import type {Config, LocalConfiguration} from 'types/config';
|
||||
import type {SaveQueueItem, SettingsDefinition} from 'types/settings';
|
||||
|
||||
import generateDefinition from './definition';
|
||||
|
||||
import './SettingsModal.scss';
|
||||
|
||||
enum SavingState {
|
||||
SAVING = 1,
|
||||
SAVED,
|
||||
DONE
|
||||
}
|
||||
|
||||
export default function SettingsModal({
|
||||
onClose,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
|
||||
const saveQueue = useRef<SaveQueueItem[]>([]);
|
||||
const saveDebounce = useRef<boolean>(false);
|
||||
const resetDebounce = useRef<boolean>(false);
|
||||
|
||||
const [savingState, setSavingState] = useState<SavingState>(SavingState.DONE);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>();
|
||||
const [config, setConfig] = useState<LocalConfiguration>();
|
||||
const [definition, setDefinition] = useState<SettingsDefinition>();
|
||||
const [cache, setCache] = useState<EmotionCache>();
|
||||
|
||||
const getConfig = useCallback(() => {
|
||||
window.desktop.getLocalConfiguration().then((result) => {
|
||||
setConfig(result);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getDefinition = useCallback(() => {
|
||||
return generateDefinition(intl).then((result) => {
|
||||
setDefinition(result);
|
||||
return result;
|
||||
});
|
||||
}, [intl, selectedCategory]);
|
||||
|
||||
const setSavingStateToDone = useCallback(() => {
|
||||
resetDebounce.current = false;
|
||||
if (savingState !== SavingState.SAVING) {
|
||||
setSavingState(SavingState.DONE);
|
||||
}
|
||||
}, [savingState]);
|
||||
|
||||
const resetSaveState = useCallback(() => {
|
||||
if (resetDebounce.current) {
|
||||
return;
|
||||
}
|
||||
resetDebounce.current = true;
|
||||
setTimeout(setSavingStateToDone, 2000);
|
||||
}, [setSavingStateToDone]);
|
||||
|
||||
const updateConfiguration = useCallback(() => {
|
||||
if (saveQueue.current.length === 0) {
|
||||
setSavingState(SavingState.SAVED);
|
||||
resetSaveState();
|
||||
}
|
||||
getConfig();
|
||||
getDefinition();
|
||||
}, [getConfig, resetSaveState]);
|
||||
|
||||
const sendSave = useCallback(() => {
|
||||
saveDebounce.current = false;
|
||||
window.desktop.updateConfiguration(saveQueue.current.splice(0, saveQueue.current.length));
|
||||
}, []);
|
||||
|
||||
const processSaveQueue = useCallback(() => {
|
||||
if (saveDebounce.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveDebounce.current = true;
|
||||
setTimeout(sendSave, 500);
|
||||
}, [sendSave]);
|
||||
|
||||
const save = useCallback((key: keyof Config, data: Config[keyof Config]) => {
|
||||
saveQueue.current.push({
|
||||
key,
|
||||
data,
|
||||
});
|
||||
setSavingState(SavingState.SAVING);
|
||||
processSaveQueue();
|
||||
}, [processSaveQueue]);
|
||||
|
||||
useEffect(() => {
|
||||
window.desktop.getNonce().then((nonce) => {
|
||||
setCache(createCache({
|
||||
key: 'react-select-cache',
|
||||
nonce,
|
||||
}));
|
||||
});
|
||||
|
||||
window.desktop.onReloadConfiguration(updateConfiguration);
|
||||
|
||||
getDefinition().then((definition) => {
|
||||
setSelectedCategory(Object.keys(definition)[0]);
|
||||
});
|
||||
getConfig();
|
||||
}, []);
|
||||
|
||||
let savingText;
|
||||
if (savingState === SavingState.SAVING) {
|
||||
savingText = (
|
||||
<div className='SettingsModal__saving'>
|
||||
<i className='icon-spinner'/>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.saving'
|
||||
defaultMessage='Saving...'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (savingState === SavingState.SAVED) {
|
||||
savingText = (
|
||||
<div className='SettingsModal__saving'>
|
||||
<i className='icon-check'/>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.changesSaved'
|
||||
defaultMessage='Changes saved'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!cache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CacheProvider value={cache}>
|
||||
<Modal
|
||||
id='settingsModal'
|
||||
className='SettingsModal'
|
||||
show={Boolean(config && definition && selectedCategory)}
|
||||
onExited={onClose}
|
||||
modalHeaderText={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.header'
|
||||
defaultMessage='Desktop App Settings'
|
||||
/>
|
||||
}
|
||||
headerContent={savingText}
|
||||
bodyDivider={true}
|
||||
bodyPadding={false}
|
||||
>
|
||||
<div className='SettingsModal__sidebar'>
|
||||
{definition && Object.entries(definition).map(([id, category]) => (
|
||||
<button
|
||||
id={`settingCategoryButton-${id}`}
|
||||
key={id}
|
||||
className={classNames('SettingsModal__category', {selected: id === selectedCategory})}
|
||||
onClick={() => setSelectedCategory(id)}
|
||||
>
|
||||
<i className={`icon icon-${category.icon}`}/>
|
||||
{category.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className='SettingsModal__content'>
|
||||
{(config && definition && selectedCategory) && definition[selectedCategory].settings.map((setting) => (setting.condition ?? true) && (
|
||||
<setting.component
|
||||
key={setting.id}
|
||||
id={setting.id}
|
||||
onSave={save}
|
||||
value={config[setting.id]}
|
||||
{...setting.props}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</CacheProvider>
|
||||
);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -133,4 +133,9 @@
|
||||
--error-text: #da6c6e;
|
||||
--mention-highlight-bg: #0d6e6e;
|
||||
--mention-highlight-link: #a4f4f4;
|
||||
|
||||
/* Border variables */
|
||||
--border-default: solid 1px rgba(var(--center-channel-color-rgb), 0.12);
|
||||
--border-light: solid 1px rgba(var(--center-channel-color-rgb), 0.08);
|
||||
--border-dark: solid 1px rgba(var(--center-channel-color-rgb), 0.16);
|
||||
}
|
||||
|
@@ -12,6 +12,10 @@
|
||||
|
||||
.Modal_body {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.Modal__body {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,97 +0,0 @@
|
||||
@use 'sass:meta';
|
||||
|
||||
.darkMode {
|
||||
@include meta.load-css('bootstrap-dark/src/bootstrap-dark.css');
|
||||
color: #fff;
|
||||
|
||||
> div.modal {
|
||||
color: #fff;
|
||||
|
||||
.modal-header .modal-title, .modal-header .close {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #191B1F;
|
||||
}
|
||||
}
|
||||
|
||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__control {
|
||||
background: #242a30;
|
||||
}
|
||||
|
||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__menu {
|
||||
background: #242a30;
|
||||
}
|
||||
|
||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__option {
|
||||
background: #242a30;
|
||||
}
|
||||
|
||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__option:hover {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
#settingsModal .modal-body {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--light) rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
.Toggle___switch {
|
||||
background: rgba(var(--center-channel-bg-rgb), 0.24);
|
||||
}
|
||||
|
||||
.Toggle___switch.disabled {
|
||||
background: rgba(var(--center-channel-bg-rgb), 0.08);
|
||||
}
|
||||
|
||||
.Input {
|
||||
color: var(--button-color);
|
||||
background-color: unset;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(var(--button-color-rgb), 0.56);
|
||||
}
|
||||
}
|
||||
|
||||
.Input_wrapper {
|
||||
color: rgba(var(--button-color-rgb), 0.56);
|
||||
}
|
||||
|
||||
.Input___info {
|
||||
color: rgba(var(--button-color-rgb), 0.56);
|
||||
}
|
||||
|
||||
.Input_fieldset {
|
||||
background-color: #191B1F;
|
||||
border: 1px solid rgba(#fff, 0.16);
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(#fff, 0.48);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: #fff;
|
||||
box-shadow: inset 0 0 0 1px #fff;
|
||||
color: var(--button-color);
|
||||
|
||||
.Input_legend {
|
||||
color: var(--button-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Input_legend {
|
||||
background-color: #191B1F;
|
||||
color: rgba(var(--button-color-rgb), 0.64);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.Input_fieldset {
|
||||
background: rgba(var(--button-color-rgb), 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +0,0 @@
|
||||
@import url("components/index.css");
|
||||
@import url("fonts.css");
|
||||
@import '~@mattermost/compass-icons/css/compass-icons.css';
|
||||
|
||||
body {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #166de0;
|
||||
border-color: #166de0;
|
||||
}
|
@@ -1,65 +0,0 @@
|
||||
.CloseButton:hover span {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.IndicatorContainer {
|
||||
display: inline-block;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.AutoSaveIndicator {
|
||||
padding: 5px 15px;
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
|
||||
.AutoSaveIndicator.AutoSaveIndicator-Leave {
|
||||
opacity: 0;
|
||||
transition: opacity 1s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
}
|
||||
|
||||
.checkbox > label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #166de0;
|
||||
border-color: #166de0;
|
||||
}
|
||||
|
||||
.SettingsPage__spellCheckerLocalesDropdown {
|
||||
margin-top: 8px;
|
||||
margin-left: 16px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#settingsModal .modal-content {
|
||||
padding: 16px;
|
||||
max-height: calc(100vh - 50px);
|
||||
}
|
||||
|
||||
#settingsModal .modal-body {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--dark) rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
#settingsModal .modal-header {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 862px) {
|
||||
#settingsModal {
|
||||
max-width: 786px;
|
||||
}
|
||||
}
|
@@ -1,15 +1,10 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/index.css';
|
||||
import 'renderer/css/settings.css';
|
||||
import 'renderer/css/modals-dark.scss';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import SettingsPage from '../../components/SettingsPage';
|
||||
import SettingsModal from '../../components/SettingsModal';
|
||||
import IntlProvider from '../../intl_provider';
|
||||
import setupDarkMode from '../darkMode';
|
||||
|
||||
@@ -23,8 +18,7 @@ const start = async () => {
|
||||
ReactDOM.render(
|
||||
(
|
||||
<IntlProvider>
|
||||
<SettingsPage
|
||||
show={true}
|
||||
<SettingsModal
|
||||
onClose={onClose}
|
||||
/>
|
||||
</IntlProvider>
|
||||
|
@@ -41,7 +41,7 @@ export type ConfigV3 = {
|
||||
notifications: {
|
||||
flashWindow: number;
|
||||
bounceIcon: boolean;
|
||||
bounceIconType: 'critical' | 'informational';
|
||||
bounceIconType: '' | 'critical' | 'informational';
|
||||
};
|
||||
showUnreadBadge: boolean;
|
||||
useSpellChecker: boolean;
|
||||
|
@@ -1,12 +1,13 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {CombinedConfig} from './config';
|
||||
import type {ReactNode, ComponentType, ComponentProps} from 'react';
|
||||
|
||||
import type {Config} from './config';
|
||||
|
||||
export type SaveQueueItem = {
|
||||
configType: 'updates' | 'appOptions';
|
||||
key: keyof CombinedConfig;
|
||||
data: CombinedConfig[keyof CombinedConfig];
|
||||
key: keyof Config;
|
||||
data: Config[keyof Config];
|
||||
};
|
||||
|
||||
export type DeveloperSettings = {
|
||||
@@ -15,3 +16,16 @@ export type DeveloperSettings = {
|
||||
disableUserActivityMonitor?: boolean;
|
||||
disableContextMenu?: boolean;
|
||||
};
|
||||
|
||||
export type SettingsDefinition = Record<string, SettingCategory>;
|
||||
export type SettingCategory = {
|
||||
title: ReactNode;
|
||||
icon: string;
|
||||
settings: Setting[];
|
||||
};
|
||||
export type Setting = {
|
||||
id: keyof Config;
|
||||
component: ComponentType<any>;
|
||||
condition?: boolean;
|
||||
props?: ComponentProps<Setting['component']>;
|
||||
};
|
||||
|
@@ -3,8 +3,9 @@
|
||||
|
||||
import type {ipcRenderer, Rectangle} from 'electron/renderer';
|
||||
|
||||
import type {CombinedConfig, LocalConfiguration, UniqueView, UniqueServer} from './config';
|
||||
import type {CombinedConfig, LocalConfiguration, UniqueView, UniqueServer, Server} from './config';
|
||||
import type {DownloadedItem, DownloadedItems, DownloadsMenuOpenEventPayload} from './downloads';
|
||||
import type {UniqueServerWithPermissions, Permissions} from './permissions';
|
||||
import type {URLValidationResult} from './server';
|
||||
import type {SaveQueueItem} from './settings';
|
||||
|
||||
@@ -55,6 +56,10 @@ declare global {
|
||||
getOrderedTabsForServer: (serverId: string) => Promise<UniqueView[]>;
|
||||
onUpdateServers: (listener: () => void) => void;
|
||||
validateServerURL: (url: string, currentId?: string) => Promise<URLValidationResult>;
|
||||
getUniqueServersWithPermissions: () => Promise<UniqueServerWithPermissions[]>;
|
||||
addServer: (server: Server) => void;
|
||||
editServer: (server: UniqueServer, permissions?: Permissions) => void;
|
||||
removeServer: (serverId: string) => void;
|
||||
|
||||
getConfiguration: () => Promise<CombinedConfig[keyof CombinedConfig] | CombinedConfig>;
|
||||
getVersion: () => Promise<{name: string; version: string}>;
|
||||
@@ -63,12 +68,12 @@ declare global {
|
||||
getFullScreenStatus: () => Promise<boolean>;
|
||||
getAvailableSpellCheckerLanguages: () => Promise<string[]>;
|
||||
getAvailableLanguages: () => Promise<string[]>;
|
||||
getLocalConfiguration: () => Promise<LocalConfiguration[keyof LocalConfiguration] | Partial<LocalConfiguration>>;
|
||||
getLocalConfiguration: () => Promise<LocalConfiguration>;
|
||||
getDownloadLocation: (downloadLocation?: string) => Promise<string>;
|
||||
getLanguageInformation: () => Promise<Language>;
|
||||
|
||||
onSynchronizeConfig: (listener: () => void) => void;
|
||||
onReloadConfiguration: (listener: () => void) => void;
|
||||
onReloadConfiguration: (listener: () => void) => () => void;
|
||||
onDarkModeChange: (listener: (darkMode: boolean) => void) => void;
|
||||
onLoadRetry: (listener: (viewId: string, retry: Date, err: string, loadUrl: string) => void) => void;
|
||||
onLoadSuccess: (listener: (viewId: string) => void) => void;
|
||||
|
Reference in New Issue
Block a user