Browse Source

feat(settings): add a settingsService to persist lang to disk

master
jamaljsr 5 years ago
parent
commit
edee284e7f
  1. 6
      src/components/layouts/AppLayout.spec.tsx
  2. 12
      src/components/layouts/LocaleSwitch.tsx
  3. 1
      src/lib/settings/index.ts
  4. 45
      src/lib/settings/settingsService.spec.ts
  5. 35
      src/lib/settings/settingsService.ts
  6. 2
      src/store/index.ts
  7. 13
      src/store/models/app.spec.ts
  8. 27
      src/store/models/app.ts
  9. 11
      src/types/index.ts
  10. 4
      src/utils/tests/renderWithProviders.tsx

6
src/components/layouts/AppLayout.spec.tsx

@ -46,9 +46,9 @@ describe('AppLayout component', () => {
expect(getByText("Let's get started!")).toBeInTheDocument();
fireEvent.mouseEnter(getByText('English'));
fireEvent.click(await findByText('Español (es-ES)'));
expect(getByText('¡Empecemos!')).toBeInTheDocument();
expect(await findByText('¡Empecemos!')).toBeInTheDocument();
fireEvent.click(getByText('English (en-US)'));
expect(getByText("Let's get started!")).toBeInTheDocument();
expect(await findByText("Let's get started!")).toBeInTheDocument();
});
it('should set language to Spanish', async () => {
@ -56,7 +56,7 @@ describe('AppLayout component', () => {
expect(getByText("Let's get started!")).toBeInTheDocument();
fireEvent.mouseEnter(getByText('English'));
fireEvent.click(await findByText('Español (es-ES)'));
expect(getByText('¡Empecemos!')).toBeInTheDocument();
expect(await findByText('¡Empecemos!')).toBeInTheDocument();
});
});
});

12
src/components/layouts/LocaleSwitch.tsx

@ -1,9 +1,10 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import styled from '@emotion/styled';
import { Button, Dropdown, Icon, Menu } from 'antd';
import { ClickParam } from 'antd/lib/menu';
import { useStoreState } from 'easy-peasy';
import { localeConfig } from 'i18n';
import { useStoreActions } from 'store';
const Styled = {
Button: styled(Button)`
@ -12,13 +13,14 @@ const Styled = {
};
const LocaleSwitch: React.FC = () => {
const { i18n } = useTranslation();
const { settings } = useStoreState(s => s.app);
const { updateSettings } = useStoreActions(s => s.app);
const changeLanguage = (e: ClickParam) => {
i18n.changeLanguage(e.key);
updateSettings({ lang: e.key });
};
const menu = (
<Menu onClick={changeLanguage} selectedKeys={[i18n.language]}>
<Menu onClick={changeLanguage} selectedKeys={[settings.lang]}>
{Object.entries(localeConfig.languages).map(([key, lang]) => (
<Menu.Item key={key}>
{lang} ({key})
@ -32,7 +34,7 @@ const LocaleSwitch: React.FC = () => {
<Dropdown overlay={menu} placement="topRight">
<Styled.Button type="link">
<Icon type="global" />
{localeConfig.languages[i18n.language]}
{localeConfig.languages[settings.lang]}
</Styled.Button>
</Dropdown>
</>

1
src/lib/settings/index.ts

@ -0,0 +1 @@
export { default as settingsService } from './settingsService';

45
src/lib/settings/settingsService.spec.ts

@ -0,0 +1,45 @@
import { AppSettings } from 'types';
import * as files from 'utils/files';
import { settingsService } from './';
jest.mock('utils/files', () => ({
write: jest.fn(),
read: jest.fn(),
exists: jest.fn(),
}));
const filesMock = files as jest.Mocked<typeof files>;
describe('SettingsService', () => {
let settings: AppSettings;
beforeEach(() => {
settings = {
lang: 'en-US',
showAllNodeVersions: true,
};
});
it('should save the settings to disk', () => {
settingsService.save(settings);
expect(filesMock.write).toBeCalledWith(
expect.stringContaining('settings.json'),
expect.stringContaining(`"lang": "en-US"`),
);
});
it('should load the settings from disk', async () => {
filesMock.exists.mockResolvedValue(true);
filesMock.read.mockResolvedValue('{ "lang": "en-US" }');
const settings = await settingsService.load();
expect(settings).toBeDefined();
expect(settings && settings.lang).toBe('en-US');
expect(filesMock.read).toBeCalledWith(expect.stringContaining('settings.json'));
});
it('should return undefined if no settings are saved', async () => {
filesMock.exists.mockResolvedValue(false);
const settings = await settingsService.load();
expect(settings).toBeUndefined();
});
});

35
src/lib/settings/settingsService.ts

@ -0,0 +1,35 @@
import { info } from 'electron-log';
import { join } from 'path';
import { AppSettings, SettingsInjection } from 'types';
import { dataPath } from 'utils/config';
import { exists, read, write } from 'utils/files';
class SettingsService implements SettingsInjection {
/**
* Saves the given settings to the file system
* @param settings the list of settings to save
*/
async save(data: AppSettings) {
const json = JSON.stringify(data, null, 2);
const path = join(dataPath, 'settings.json');
await write(path, json);
info(`saved settings to '${path}'`, json);
}
/**
* Loads a list of settings from the file system
*/
async load(): Promise<AppSettings | undefined> {
const path = join(dataPath, 'settings.json');
if (await exists(path)) {
const json = await read(path);
const data = JSON.parse(json);
info(`loaded app settings from '${path}'`, data);
return data;
} else {
info(`skipped loading app settings because the file '${path}' doesn't exist`);
}
}
}
export default new SettingsService();

2
src/store/index.ts

@ -6,6 +6,7 @@ import { bitcoindService } from 'lib/bitcoin';
import { dockerService } from 'lib/docker';
import { createIpcSender } from 'lib/ipc/ipcService';
import { LightningFactory } from 'lib/lightning';
import { settingsService } from 'lib/settings';
import { createModel, RootModel } from 'store/models';
import { StoreInjections } from 'types';
@ -55,6 +56,7 @@ export const createReduxStore = (options?: {
// see https://easy-peasy.now.sh/docs/testing/testing-components.html#mocking-calls-to-services
const injections: StoreInjections = {
ipc: createIpcSender('AppModel', 'app'),
settingsService,
dockerService,
bitcoindService,
lightningFactory: new LightningFactory(),

13
src/store/models/app.spec.ts

@ -7,6 +7,9 @@ import networkModel from './network';
const mockDockerService = injections.dockerService as jest.Mocked<
typeof injections.dockerService
>;
const mockSettingsService = injections.settingsService as jest.Mocked<
typeof injections.settingsService
>;
describe('App model', () => {
const rootModel = {
@ -22,15 +25,25 @@ describe('App model', () => {
store = createStore(rootModel, { injections });
mockDockerService.getVersions.mockResolvedValue({ docker: '', compose: '' });
mockDockerService.loadNetworks.mockResolvedValue({ networks: [], charts: {} });
mockSettingsService.load.mockResolvedValue({
lang: 'en-US',
showAllNodeVersions: true,
});
});
it('should initialize', async () => {
await store.getActions().app.initialize();
expect(store.getState().app.initialized).toBe(true);
expect(mockSettingsService.load).toBeCalledTimes(1);
expect(mockDockerService.getVersions).toBeCalledTimes(1);
expect(mockDockerService.loadNetworks).toBeCalledTimes(1);
});
it('should update settings', async () => {
store.getActions().app.updateSettings({ showAllNodeVersions: true });
expect(store.getState().app.settings.showAllNodeVersions).toBe(true);
});
describe('with mocked actions', () => {
beforeEach(() => {
// reset the store before each test run

27
src/store/models/app.ts

@ -1,16 +1,14 @@
import { getI18n } from 'react-i18next';
import { shell } from 'electron';
import { notification } from 'antd';
import { ArgsProps } from 'antd/lib/notification';
import { push } from 'connected-react-router';
import { Action, action, Thunk, thunk } from 'easy-peasy';
import { ipcChannels } from 'shared';
import { DockerVersions, StoreInjections } from 'types';
import { localeConfig } from 'i18n';
import { AppSettings, DockerVersions, StoreInjections } from 'types';
import { RootModel } from './';
export interface AppSettings {
showAllNodeVersions: boolean;
}
export interface NotifyOptions {
message: string;
description?: string;
@ -24,6 +22,8 @@ export interface AppModel {
dockerImages: string[];
setInitialized: Action<AppModel, boolean>;
setSettings: Action<AppModel, Partial<AppSettings>>;
loadSettings: Thunk<AppModel, any, StoreInjections, RootModel>;
updateSettings: Thunk<AppModel, Partial<AppSettings>, StoreInjections, RootModel>;
initialize: Thunk<AppModel, any, StoreInjections, RootModel>;
setDockerVersions: Action<AppModel, DockerVersions>;
getDockerVersions: Thunk<AppModel, { throwErr?: boolean }, StoreInjections, RootModel>;
@ -40,6 +40,7 @@ const appModel: AppModel = {
// state properties
initialized: false,
settings: {
lang: localeConfig.fallbackLng,
showAllNodeVersions: false,
},
dockerVersions: { docker: '', compose: '' },
@ -48,7 +49,8 @@ const appModel: AppModel = {
setInitialized: action((state, initialized) => {
state.initialized = initialized;
}),
initialize: thunk(async (actions, payload, { getStoreActions }) => {
initialize: thunk(async (actions, _, { getStoreActions }) => {
await actions.loadSettings();
await getStoreActions().network.load();
await actions.getDockerVersions({});
await actions.getDockerImages();
@ -60,6 +62,19 @@ const appModel: AppModel = {
...settings,
};
}),
loadSettings: thunk(async (actions, _, { injections }) => {
const settings = await injections.settingsService.load();
if (settings) {
actions.setSettings(settings);
await getI18n().changeLanguage(settings.lang);
}
}),
updateSettings: thunk(async (actions, updates, { injections, getState }) => {
actions.setSettings(updates);
const { settings } = getState();
await injections.settingsService.save(settings);
if (updates.lang) await getI18n().changeLanguage(settings.lang);
}),
setDockerVersions: action((state, versions) => {
state.dockerVersions = versions;
}),

11
src/types/index.ts

@ -22,6 +22,16 @@ export interface Network {
};
}
export interface AppSettings {
lang: string;
showAllNodeVersions: boolean;
}
export interface SettingsInjection {
save: (settings: AppSettings) => Promise<void>;
load: () => Promise<AppSettings | undefined>;
}
export interface DockerVersions {
docker: string;
compose: string;
@ -77,6 +87,7 @@ export interface LightningFactoryInjection {
export interface StoreInjections {
ipc: IpcSender;
settingsService: SettingsInjection;
dockerService: DockerLibrary;
bitcoindService: BitcoindLibrary;
lightningFactory: LightningFactoryInjection;

4
src/utils/tests/renderWithProviders.tsx

@ -23,6 +23,10 @@ export const lightningServiceMock: jest.Mocked<LightningService> = {
// injections allow you to mock the dependencies of redux store actions
export const injections: StoreInjections = {
ipc: jest.fn(),
settingsService: {
load: jest.fn(),
save: jest.fn(),
},
dockerService: {
getVersions: jest.fn(),
getImages: jest.fn(),

Loading…
Cancel
Save