Migrated all servers services to TS

This commit is contained in:
Alejandro Celaya 2020-08-29 14:16:37 +02:00
parent aee4c2d02f
commit 64a968711c
12 changed files with 66 additions and 53 deletions

5
csvjson.d.ts vendored
View file

@ -1,5 +0,0 @@
declare module 'csvjson' {
export declare class CsvJson {
public toObject<T>(content: string): T[];
}
}

View file

@ -2,4 +2,11 @@ declare module 'event-source-polyfill' {
export const EventSourcePolyfill: any; export const EventSourcePolyfill: any;
} }
declare module 'csvjson' {
export declare class CsvJson {
public toObject<T>(content: string): T[];
public toCSV<T>(data: T[], options: { headers: string }): string;
}
}
declare module '*.png' declare module '*.png'

View file

@ -24,7 +24,7 @@ const mapActionService = (map: ActionMap, actionName: string): ActionMap => ({
// Wrap actual action service in a function so that it is lazily created the first time it is called // Wrap actual action service in a function so that it is lazily created the first time it is called
[actionName]: lazyService(container, actionName), [actionName]: lazyService(container, actionName),
}); });
const connect: ConnectDecorator = (propsFromState: string[], actionServiceNames: string[] = []) => const connect: ConnectDecorator = (propsFromState: string[] | null, actionServiceNames: string[] = []) =>
reduxConnect( reduxConnect(
propsFromState ? pick(propsFromState) : null, propsFromState ? pick(propsFromState) : null,
actionServiceNames.reduce(mapActionService, {}), actionServiceNames.reduce(mapActionService, {}),

View file

@ -35,6 +35,6 @@ export interface ShlinkState {
settings: Settings; settings: Settings;
} }
export type ConnectDecorator = (props: string[], actions?: string[]) => any; export type ConnectDecorator = (props: string[] | null, actions?: string[]) => any;
export type GetState = () => ShlinkState; export type GetState = () => ShlinkState;

View file

@ -1,6 +1,9 @@
import { dissoc, head, keys, values } from 'ramda'; import { dissoc, head, keys, values } from 'ramda';
import { CsvJson } from 'csvjson';
import LocalStorage from '../../utils/services/LocalStorage';
import { ServersMap } from '../data';
const saveCsv = (window, csv) => { const saveCsv = (window: Window, csv: string) => {
const { navigator, document } = window; const { navigator, document } = window;
const filename = 'shlink-servers.csv'; const filename = 'shlink-servers.csv';
const blob = new Blob([ csv ], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([ csv ], { type: 'text/csv;charset=utf-8;' });
@ -25,14 +28,14 @@ const saveCsv = (window, csv) => {
}; };
export default class ServersExporter { export default class ServersExporter {
constructor(storage, window, csvjson) { public constructor(
this.storage = storage; private readonly storage: LocalStorage,
this.window = window; private readonly window: Window,
this.csvjson = csvjson; private readonly csvjson: CsvJson,
} ) {}
exportServers = async () => { public readonly exportServers = async () => {
const servers = values(this.storage.get('servers') || {}).map(dissoc('id')); const servers = values(this.storage.get<ServersMap>('servers') || {}).map(dissoc('id'));
try { try {
const csv = this.csvjson.toCSV(servers, { const csv = this.csvjson.toCSV(servers, {

View file

@ -6,7 +6,7 @@ const CSV_MIME_TYPE = 'text/csv';
export default class ServersImporter { export default class ServersImporter {
public constructor(private readonly csvjson: CsvJson, private readonly fileReaderFactory: () => FileReader) {} public constructor(private readonly csvjson: CsvJson, private readonly fileReaderFactory: () => FileReader) {}
public importServersFromFile = async (file?: File | null): Promise<ServerData[]> => { public readonly importServersFromFile = async (file?: File | null): Promise<ServerData[]> => {
if (!file || file.type !== CSV_MIME_TYPE) { if (!file || file.type !== CSV_MIME_TYPE) {
throw new Error('No file provided or file is not a CSV'); throw new Error('No file provided or file is not a CSV');
} }

View file

@ -1,4 +1,5 @@
import csvjson from 'csvjson'; import csvjson from 'csvjson';
import Bottle, { Decorator } from 'bottlejs';
import CreateServer from '../CreateServer'; import CreateServer from '../CreateServer';
import ServersDropdown from '../ServersDropdown'; import ServersDropdown from '../ServersDropdown';
import DeleteServerModal from '../DeleteServerModal'; import DeleteServerModal from '../DeleteServerModal';
@ -10,10 +11,11 @@ import { createServer, createServers, deleteServer, editServer } from '../reduce
import { fetchServers } from '../reducers/remoteServers'; import { fetchServers } from '../reducers/remoteServers';
import ForServerVersion from '../helpers/ForServerVersion'; import ForServerVersion from '../helpers/ForServerVersion';
import { ServerError } from '../helpers/ServerError'; import { ServerError } from '../helpers/ServerError';
import { ConnectDecorator } from '../../container/types';
import ServersImporter from './ServersImporter'; import ServersImporter from './ServersImporter';
import ServersExporter from './ServersExporter'; import ServersExporter from './ServersExporter';
const provideServices = (bottle, connect, withRouter) => { const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: Decorator) => {
// Components // Components
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useStateFlagTimeout'); bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useStateFlagTimeout');
bottle.decorator('CreateServer', connect([ 'selectedServer' ], [ 'createServer', 'resetSelectedServer' ])); bottle.decorator('CreateServer', connect([ 'selectedServer' ], [ 'createServer', 'resetSelectedServer' ]));

View file

@ -0,0 +1,14 @@
const PREFIX = 'shlink';
const buildPath = (path: string) => `${PREFIX}.${path}`;
export default class LocalStorage {
public constructor(private readonly localStorage: Storage) {}
public readonly get = <T>(key: string): T => {
const item = this.localStorage.getItem(buildPath(key));
return item ? JSON.parse(item) : undefined;
};
public readonly set = (key: string, value: any) => this.localStorage.setItem(buildPath(key), JSON.stringify(value));
}

View file

@ -1,16 +0,0 @@
const PREFIX = 'shlink';
const buildPath = (path) => `${PREFIX}.${path}`;
export default class Storage {
constructor(localStorage) {
this.localStorage = localStorage;
}
get = (key) => {
const item = this.localStorage.getItem(buildPath(key));
return item ? JSON.parse(item) : undefined;
};
set = (key, value) => this.localStorage.setItem(buildPath(key), JSON.stringify(value));
}

View file

@ -1,13 +1,13 @@
import axios from 'axios'; import axios from 'axios';
import Bottle from 'bottlejs'; import Bottle from 'bottlejs';
import { useStateFlagTimeout } from '../helpers/hooks'; import { useStateFlagTimeout } from '../helpers/hooks';
import Storage from './Storage'; import LocalStorage from './LocalStorage';
import ColorGenerator from './ColorGenerator'; import ColorGenerator from './ColorGenerator';
import buildShlinkApiClient from './ShlinkApiClientBuilder'; import buildShlinkApiClient from './ShlinkApiClientBuilder';
const provideServices = (bottle: Bottle) => { const provideServices = (bottle: Bottle) => {
bottle.constant('localStorage', (global as any).localStorage); bottle.constant('localStorage', (global as any).localStorage);
bottle.service('Storage', Storage, 'localStorage'); bottle.service('Storage', LocalStorage, 'localStorage');
bottle.service('ColorGenerator', ColorGenerator, 'Storage'); bottle.service('ColorGenerator', ColorGenerator, 'Storage');
bottle.constant('axios', axios); bottle.constant('axios', axios);

View file

@ -1,4 +1,7 @@
import { Mock } from 'ts-mockery';
import { CsvJson } from 'csvjson';
import ServersExporter from '../../../src/servers/services/ServersExporter'; import ServersExporter from '../../../src/servers/services/ServersExporter';
import LocalStorage from '../../../src/utils/services/LocalStorage';
describe('ServersExporter', () => { describe('ServersExporter', () => {
const createLinkMock = () => ({ const createLinkMock = () => ({
@ -6,7 +9,7 @@ describe('ServersExporter', () => {
click: jest.fn(), click: jest.fn(),
style: {}, style: {},
}); });
const createWindowMock = (isIe10 = true) => ({ const createWindowMock = (isIe10 = true) => Mock.of<Window>({
navigator: { navigator: {
msSaveBlob: isIe10 ? jest.fn() : undefined, msSaveBlob: isIe10 ? jest.fn() : undefined,
}, },
@ -18,7 +21,7 @@ describe('ServersExporter', () => {
}, },
}, },
}); });
const storageMock = { const storageMock = Mock.of<LocalStorage>({
get: jest.fn(() => ({ get: jest.fn(() => ({
abc123: { abc123: {
id: 'abc123', id: 'abc123',
@ -29,8 +32,8 @@ describe('ServersExporter', () => {
name: 'bar', name: 'bar',
}, },
})), })),
}; });
const createCsvjsonMock = (throwError = false) => ({ const createCsvjsonMock = (throwError = false) => Mock.of<CsvJson>({
toCSV: jest.fn(() => { toCSV: jest.fn(() => {
if (throwError) { if (throwError) {
throw new Error(''); throw new Error('');
@ -41,13 +44,14 @@ describe('ServersExporter', () => {
}); });
describe('exportServers', () => { describe('exportServers', () => {
let originalConsole; let originalConsole: Console;
const error = jest.fn();
beforeEach(() => { beforeEach(() => {
originalConsole = global.console; originalConsole = global.console;
global.console = { error: jest.fn() }; global.console = Mock.of<Console>({ error });
global.Blob = class Blob {}; (global as any).Blob = class Blob {}; // eslint-disable-line @typescript-eslint/no-extraneous-class
global.URL = { createObjectURL: () => '' }; (global as any).URL = { createObjectURL: () => '' };
}); });
afterEach(() => { afterEach(() => {
global.console = originalConsole; global.console = originalConsole;
@ -57,34 +61,38 @@ describe('ServersExporter', () => {
it('logs an error if something fails', () => { it('logs an error if something fails', () => {
const csvjsonMock = createCsvjsonMock(true); const csvjsonMock = createCsvjsonMock(true);
const exporter = new ServersExporter(storageMock, createWindowMock(), csvjsonMock); const exporter = new ServersExporter(storageMock, createWindowMock(), csvjsonMock);
const { toCSV } = csvjsonMock;
exporter.exportServers(); exporter.exportServers();
expect(global.console.error).toHaveBeenCalledTimes(1); expect(error).toHaveBeenCalledTimes(1);
expect(csvjsonMock.toCSV).toHaveBeenCalledTimes(1); expect(toCSV).toHaveBeenCalledTimes(1);
}); });
it('makes use of msSaveBlob API when available', () => { it('makes use of msSaveBlob API when available', () => {
const windowMock = createWindowMock(); const windowMock = createWindowMock();
const exporter = new ServersExporter(storageMock, windowMock, createCsvjsonMock()); const exporter = new ServersExporter(storageMock, windowMock, createCsvjsonMock());
const { navigator: { msSaveBlob }, document: { createElement } } = windowMock;
exporter.exportServers(); exporter.exportServers();
expect(storageMock.get).toHaveBeenCalledTimes(1); expect(storageMock.get).toHaveBeenCalledTimes(1);
expect(windowMock.navigator.msSaveBlob).toHaveBeenCalledTimes(1); expect(msSaveBlob).toHaveBeenCalledTimes(1);
expect(windowMock.document.createElement).not.toHaveBeenCalled(); expect(createElement).not.toHaveBeenCalled();
}); });
it('makes use of download link API when available', () => { it('makes use of download link API when available', () => {
const windowMock = createWindowMock(false); const windowMock = createWindowMock(false);
const exporter = new ServersExporter(storageMock, windowMock, createCsvjsonMock()); const exporter = new ServersExporter(storageMock, windowMock, createCsvjsonMock());
const { document: { createElement, body } } = windowMock;
const { appendChild, removeChild } = body;
exporter.exportServers(); exporter.exportServers();
expect(storageMock.get).toHaveBeenCalledTimes(1); expect(storageMock.get).toHaveBeenCalledTimes(1);
expect(windowMock.document.createElement).toHaveBeenCalledTimes(1); expect(createElement).toHaveBeenCalledTimes(1);
expect(windowMock.document.body.appendChild).toHaveBeenCalledTimes(1); expect(appendChild).toHaveBeenCalledTimes(1);
expect(windowMock.document.body.removeChild).toHaveBeenCalledTimes(1); expect(removeChild).toHaveBeenCalledTimes(1);
}); });
}); });
}); });

View file

@ -1,6 +1,6 @@
import Storage from '../../../src/utils/services/Storage'; import LocalStorage from '../../../src/utils/services/LocalStorage';
describe('Storage', () => { describe('LocalStorage', () => {
const localStorageMock = { const localStorageMock = {
getItem: jest.fn((key) => key === 'shlink.foo' ? JSON.stringify({ foo: 'bar' }) : null), getItem: jest.fn((key) => key === 'shlink.foo' ? JSON.stringify({ foo: 'bar' }) : null),
setItem: jest.fn(), setItem: jest.fn(),
@ -11,7 +11,7 @@ describe('Storage', () => {
localStorageMock.getItem.mockClear(); localStorageMock.getItem.mockClear();
localStorageMock.setItem.mockReset(); localStorageMock.setItem.mockReset();
storage = new Storage(localStorageMock); storage = new LocalStorage(localStorageMock);
}); });
describe('set', () => { describe('set', () => {