actual/packages/desktop-client/src/components/Settings.js
Tom French 9c0df36e16
Sort import in alphabetical order (#238)
* style: enforce sorting of imports

* style: alphabetize imports

* style: merge duplicated imports
2022-09-02 15:07:24 +01:00

587 lines
16 KiB
JavaScript

import React, { useState, useEffect, useRef } from 'react';
import { connect } from 'react-redux';
import { Route, Switch, Redirect } from 'react-router-dom';
import { css } from 'glamor';
import * as actions from 'loot-core/src/client/actions';
import Platform from 'loot-core/src/client/platform';
import { send, listen } from 'loot-core/src/platform/client/fetch';
import { numberFormats } from 'loot-core/src/shared/util';
import { Information } from 'loot-design/src/components/alerts';
import {
View,
Text,
Button,
ButtonWithLoading,
AnchorLink
} from 'loot-design/src/components/common';
import { styles, colors } from 'loot-design/src/style';
import ExpandArrow from 'loot-design/src/svg/ExpandArrow';
import useServerVersion from '../hooks/useServerVersion';
let dateFormats = [
{ value: 'MM/dd/yyyy', label: 'MM/DD/YYYY' },
{ value: 'dd/MM/yyyy', label: 'DD/MM/YYYY' },
{ value: 'yyyy-MM-dd', label: 'YYYY-MM-DD' },
{ value: 'MM.dd.yyyy', label: 'MM.DD.YYYY' },
{ value: 'dd.MM.yyyy', label: 'DD.MM.YYYY' }
];
function Title({ name, style }) {
return (
<View
style={[
{ fontSize: 20, fontWeight: 500, marginBottom: 20, flexShrink: 0 },
style
]}
>
{name}
</View>
);
}
function Advanced({ prefs, userData, pushModal, resetSync }) {
let [expanded, setExpanded] = useState(true);
let [resetting, setResetting] = useState(false);
let [resettingCache, setResettingCache] = useState(false);
async function onResetSync() {
setResetting(true);
await resetSync();
setResetting(false);
}
async function onResetCache() {
setResettingCache(true);
await send('reset-budget-cache');
setResettingCache(false);
}
return (
<View style={{ alignItems: 'flex-start', marginTop: 55 }}>
<View
style={[
{
fontSize: 15,
marginBottom: 20,
flexDirection: 'row',
alignItems: 'center'
},
styles.staticText
]}
onClick={() => setExpanded(!expanded)}
>
<ExpandArrow
width={8}
height={8}
style={{
marginRight: 5,
transition: 'transform .2s',
transform: !expanded && 'rotateZ(-90deg)'
}}
/>
Advanced
</View>
{expanded && (
<View style={{ marginBottom: 20, alignItems: 'flex-start' }}>
<Text>
<strong>Budget ID</strong>: {prefs.id}
</Text>
<View
style={{
backgroundColor: colors.n9,
alignItems: 'flex-start',
padding: 15,
borderRadius: 4,
marginTop: 20,
border: '1px solid ' + colors.n8
}}
>
<Text style={{ marginBottom: 10, width: 500, lineHeight: 1.5 }}>
<strong>Reset budget cache</strong> will clear all cached values
for the budget and recalculate the entire budget. All values in
the budget are cached for performance reasons, and if there is a
bug in the cache you won't see correct values. There is no danger
in resetting the cache. Hopefully you never have to do this.
</Text>
<ButtonWithLoading loading={resettingCache} onClick={onResetCache}>
Reset budget cache
</ButtonWithLoading>
</View>
<View
style={{
backgroundColor: colors.n9,
alignItems: 'flex-start',
padding: 15,
borderRadius: 4,
marginTop: 20,
border: '1px solid ' + colors.n8
}}
>
<Text style={{ marginBottom: 10, width: 500, lineHeight: 1.5 }}>
<strong>Reset sync</strong> will remove all local data used to
track changes for syncing, and create a fresh sync id on our
server. This file on other devices will have to be re-downloaded
to use the new sync id. Use this if there is a problem with
syncing and you want to start fresh.
</Text>
<ButtonWithLoading loading={resetting} onClick={onResetSync}>
Reset sync
</ButtonWithLoading>
<Text style={{ marginTop: 15, color: colors.n4, fontSize: 12 }}>
Sync ID: {prefs.groupId || '(none)'}
</Text>
</View>
</View>
)}
</View>
);
}
function GlobalSettings({
globalPrefs,
userData,
saveGlobalPrefs,
pushModal,
closeBudget
}) {
let [documentDirChanged, setDirChanged] = useState(false);
let dirScrolled = useRef(null);
useEffect(() => {
if (dirScrolled.current) {
dirScrolled.current.scrollTo(10000, 0);
}
}, []);
async function onChooseDocumentDir() {
let res = await window.Actual.openFileDialog({
properties: ['openDirectory']
});
if (res) {
saveGlobalPrefs({ documentDir: res[0] });
setDirChanged(true);
}
}
function onAutoUpdate(e) {
saveGlobalPrefs({ autoUpdate: e.target.checked });
}
function onTrackUsage(e) {
saveGlobalPrefs({ trackUsage: e.target.checked });
}
return (
<View>
<View>
<Title name="General" />
{!Platform.isBrowser && (
<View
style={{
flexDirection: 'row',
maxWidth: 550,
alignItems: 'center',
overflow: 'hidden'
}}
>
<Text style={{ flexShrink: 0 }}>Store files here: </Text>
<Text
innerRef={dirScrolled}
style={{
backgroundColor: 'white',
padding: '7px 10px',
borderRadius: 4,
marginLeft: 5,
overflow: 'auto',
whiteSpace: 'nowrap',
// TODO: When we update electron, we should be able to
// remove this. In previous versions of Chrome, once the
// scrollbar appears it never goes away
'::-webkit-scrollbar': { display: 'none' }
}}
>
{globalPrefs.documentDir}
</Text>
<Button
primary
onClick={onChooseDocumentDir}
style={{
fontSize: 14,
marginLeft: 5,
flexShrink: 0,
alignSelf: 'flex-start'
}}
>
Change location
</Button>
</View>
)}
{documentDirChanged && (
<Information style={{ marginTop: 10 }}>
A restart is required for this change to take effect
</Information>
)}
<View
style={{
flexDirection: 'row',
marginTop: 30,
alignItems: 'flex-start'
}}
>
<input
type="checkbox"
checked={globalPrefs.autoUpdate}
style={{ marginRight: 5 }}
onChange={onAutoUpdate}
/>
<View>
<Text style={{ fontSize: 15 }}>
Automatically check for updates
</Text>
<View
style={{
color: colors.n2,
marginTop: 10,
maxWidth: 600,
lineHeight: '1.4em'
}}
>
By default, Actual will automatically apply new updates as they
are available. Disabling this will avoid updating Actual. You will
need to go to the About menu to manually check for updates.
</View>
</View>
</View>
</View>
<View style={{ marginTop: 30 }}>
<Title name="Privacy" />
<View
style={{
flexDirection: 'row',
marginTop: 30,
alignItems: 'flex-start'
}}
>
<input
type="checkbox"
checked={globalPrefs.trackUsage}
style={{ marginRight: 5 }}
onChange={onTrackUsage}
/>
<View>
<Text style={{ fontSize: 15 }}>
Send basic usage statistics back to Actual{"'"}s servers
</Text>
<View
style={{
color: colors.n2,
marginTop: 10,
maxWidth: 600,
lineHeight: '1.4em'
}}
>
We don{"'"}t track anything specific &mdash; only the fact that
you{"'"}ve opened Actual. This helps by giving us important
feedback about how popular new features are.
</View>
</View>
</View>
</View>
</View>
);
}
function FileSettings({
savePrefs,
prefs,
userData,
localServerURL,
pushModal,
resetSync,
setAppState,
signOut
}) {
function onDateFormat(e) {
let format = e.target.value;
savePrefs({ dateFormat: format });
}
function onNumberFormat(e) {
let format = e.target.value;
savePrefs({ numberFormat: format });
}
function onChangeKey() {
pushModal('create-encryption-key', { recreate: true });
}
async function onExport() {
let data = await send('export-budget');
window.Actual.saveFile(data, `${prefs.id}.zip`, 'Export budget');
}
let dateFormat = prefs.dateFormat || 'MM/dd/yyyy';
let numberFormat = prefs.numberFormat || 'comma-dot';
return (
<View>
<View style={{ marginTop: 30 }}>
<Title name="Formatting" />
<Text>
Date format:{' '}
<select
{...css({ marginLeft: 5, fontSize: 14 })}
onChange={onDateFormat}
>
{dateFormats.map(f => (
<option value={f.value} selected={f.value === dateFormat}>
{f.label}
</option>
))}
</select>
</Text>
<Text style={{ marginTop: 20 }}>
Number format:{' '}
<select
{...css({ marginLeft: 5, fontSize: 14 })}
onChange={onNumberFormat}
>
{numberFormats.map(f => (
<option value={f.value} selected={f.value === numberFormat}>
{f.label}
</option>
))}
</select>
</Text>
</View>
<View style={{ marginTop: 30 }}>
<Title name="Encryption" />
<View style={{ flexDirection: 'row' }}>
<View>
<Text style={{ fontWeight: 700, fontSize: 15 }}>
End-to-end encryption
</Text>
<View
style={{
color: colors.n2,
marginTop: 10,
maxWidth: 600,
lineHeight: '1.4em'
}}
>
{prefs.encryptKeyId ? (
<Text>
<Text style={{ color: colors.g4, fontWeight: 600 }}>
Encryption is turned on.
</Text>{' '}
Your data is encrypted with a key that only you have before
sending it out to the cloud . Local data remains unencrypted
so if you forget your password you can re-encrypt it.
<Button
style={{ marginTop: 10 }}
onClick={() => onChangeKey()}
>
Generate new key
</Button>
</Text>
) : (
<View style={{ alignItems: 'flex-start' }}>
<Text style={{ lineHeight: '1.4em' }}>
Encryption is not enabled. Any data on our servers is still
stored safely and securely, but it's not end-to-end
encrypted which means we have the ability to read it (but we
won't). If you want, you can use a password to encrypt your
data on our servers.
</Text>
<Button
style={{ marginTop: 10 }}
onClick={() => {
alert(
'End-to-end encryption is not supported on the self-hosted service yet'
);
// pushModal('create-encryption-key');
}}
>
Enable encryption
</Button>
</View>
)}
</View>
</View>
</View>
</View>
<View style={{ marginTop: 30, alignItems: 'flex-start' }}>
<Title name="Export" />
<Button onClick={onExport}>Export data</Button>
</View>
<Advanced
prefs={prefs}
userData={userData}
pushModal={pushModal}
resetSync={resetSync}
/>
</View>
);
}
function SettingsLink({ to, name, style, first, last }) {
return (
<AnchorLink
to={to}
style={[
{
fontSize: 14,
padding: '6px 10px',
borderBottom: '2px solid transparent',
textDecoration: 'none',
borderRadius: first ? '4px 0 0 4px' : last ? '0 4px 4px 0' : 4,
border: '1px solid ' + colors.n4,
color: colors.n3
},
style
]}
activeStyle={{
backgroundColor: colors.p6,
borderColor: colors.p6,
color: 'white'
}}
>
{name}
</AnchorLink>
);
}
function Version() {
const version = useServerVersion();
return (
<Text
style={[
{
alignSelf: 'center',
color: colors.n7,
':hover': { color: colors.n2 },
padding: '6px 10px'
},
styles.staticText,
styles.smallText
]}
>
{`App: v${window.Actual.ACTUAL_VERSION} | Server: ${version}`}
</Text>
);
}
class Settings extends React.Component {
componentDidMount() {
this.unlisten = listen('prefs-updated', () => {
this.props.loadPrefs();
});
this.props.getUserData();
this.props.loadPrefs();
}
componentWillUnmount() {
this.unlisten();
}
render() {
let { prefs, globalPrefs, localServerURL, userData, match } = this.props;
return (
<View style={[styles.page, { overflow: 'hidden', fontSize: 14 }]}>
<View
style={{
flexDirection: 'row',
alignSelf: 'center',
margin: '15px 0 5px 0'
}}
>
<SettingsLink to={`${match.path}/file`} name="File" first={true} />
<SettingsLink to={`${match.path}/global`} name="Global" last={true} />
</View>
<View
style={{
flexDirection: 'row',
alignSelf: 'center',
margin: '0 0 10px 0'
}}
>
<Version />
</View>
<View
style={[
styles.pageContent,
{
alignItems: 'flex-start',
flex: 1,
overflow: 'auto',
paddingBottom: 20
}
]}
>
<View style={{ flexShrink: 0 }}>
<Switch>
<Route path={`${match.path}/`} exact>
<Redirect to={`${match.path}/file`} />
</Route>
<Route path={`${match.path}/global`}>
<GlobalSettings
globalPrefs={globalPrefs}
userData={userData}
saveGlobalPrefs={this.props.saveGlobalPrefs}
pushModal={this.props.pushModal}
closeBudget={this.props.closeBudget}
/>
</Route>
<Route path={`${match.path}/file`}>
<FileSettings
prefs={prefs}
localServerURL={localServerURL}
userData={userData}
pushModal={this.props.pushModal}
savePrefs={this.props.savePrefs}
setAppState={this.props.setAppState}
signOut={this.props.signOut}
resetSync={this.props.resetSync}
/>
</Route>
</Switch>
</View>
</View>
</View>
);
}
}
export default connect(
state => ({
prefs: state.prefs.local,
globalPrefs: state.prefs.global,
localServerURL: state.account.localServerURL,
userData: state.user.data
}),
actions
)(Settings);