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;
}
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'

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
[actionName]: lazyService(container, actionName),
});
const connect: ConnectDecorator = (propsFromState: string[], actionServiceNames: string[] = []) =>
const connect: ConnectDecorator = (propsFromState: string[] | null, actionServiceNames: string[] = []) =>
reduxConnect(
propsFromState ? pick(propsFromState) : null,
actionServiceNames.reduce(mapActionService, {}),

View file

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

View file

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

View file

@ -6,7 +6,7 @@ const CSV_MIME_TYPE = 'text/csv';
export default class ServersImporter {
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) {
throw new Error('No file provided or file is not a CSV');
}

View file

@ -1,4 +1,5 @@
import csvjson from 'csvjson';
import Bottle, { Decorator } from 'bottlejs';
import CreateServer from '../CreateServer';
import ServersDropdown from '../ServersDropdown';
import DeleteServerModal from '../DeleteServerModal';
@ -10,10 +11,11 @@ import { createServer, createServers, deleteServer, editServer } from '../reduce
import { fetchServers } from '../reducers/remoteServers';
import ForServerVersion from '../helpers/ForServerVersion';
import { ServerError } from '../helpers/ServerError';
import { ConnectDecorator } from '../../container/types';
import ServersImporter from './ServersImporter';
import ServersExporter from './ServersExporter';
const provideServices = (bottle, connect, withRouter) => {
const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: Decorator) => {
// Components
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useStateFlagTimeout');
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 Bottle from 'bottlejs';
import { useStateFlagTimeout } from '../helpers/hooks';
import Storage from './Storage';
import LocalStorage from './LocalStorage';
import ColorGenerator from './ColorGenerator';
import buildShlinkApiClient from './ShlinkApiClientBuilder';
const provideServices = (bottle: Bottle) => {
bottle.constant('localStorage', (global as any).localStorage);
bottle.service('Storage', Storage, 'localStorage');
bottle.service('Storage', LocalStorage, 'localStorage');
bottle.service('ColorGenerator', ColorGenerator, 'Storage');
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 LocalStorage from '../../../src/utils/services/LocalStorage';
describe('ServersExporter', () => {
const createLinkMock = () => ({
@ -6,7 +9,7 @@ describe('ServersExporter', () => {
click: jest.fn(),
style: {},
});
const createWindowMock = (isIe10 = true) => ({
const createWindowMock = (isIe10 = true) => Mock.of<Window>({
navigator: {
msSaveBlob: isIe10 ? jest.fn() : undefined,
},
@ -18,7 +21,7 @@ describe('ServersExporter', () => {
},
},
});
const storageMock = {
const storageMock = Mock.of<LocalStorage>({
get: jest.fn(() => ({
abc123: {
id: 'abc123',
@ -29,8 +32,8 @@ describe('ServersExporter', () => {
name: 'bar',
},
})),
};
const createCsvjsonMock = (throwError = false) => ({
});
const createCsvjsonMock = (throwError = false) => Mock.of<CsvJson>({
toCSV: jest.fn(() => {
if (throwError) {
throw new Error('');
@ -41,13 +44,14 @@ describe('ServersExporter', () => {
});
describe('exportServers', () => {
let originalConsole;
let originalConsole: Console;
const error = jest.fn();
beforeEach(() => {
originalConsole = global.console;
global.console = { error: jest.fn() };
global.Blob = class Blob {};
global.URL = { createObjectURL: () => '' };
global.console = Mock.of<Console>({ error });
(global as any).Blob = class Blob {}; // eslint-disable-line @typescript-eslint/no-extraneous-class
(global as any).URL = { createObjectURL: () => '' };
});
afterEach(() => {
global.console = originalConsole;
@ -57,34 +61,38 @@ describe('ServersExporter', () => {
it('logs an error if something fails', () => {
const csvjsonMock = createCsvjsonMock(true);
const exporter = new ServersExporter(storageMock, createWindowMock(), csvjsonMock);
const { toCSV } = csvjsonMock;
exporter.exportServers();
expect(global.console.error).toHaveBeenCalledTimes(1);
expect(csvjsonMock.toCSV).toHaveBeenCalledTimes(1);
expect(error).toHaveBeenCalledTimes(1);
expect(toCSV).toHaveBeenCalledTimes(1);
});
it('makes use of msSaveBlob API when available', () => {
const windowMock = createWindowMock();
const exporter = new ServersExporter(storageMock, windowMock, createCsvjsonMock());
const { navigator: { msSaveBlob }, document: { createElement } } = windowMock;
exporter.exportServers();
expect(storageMock.get).toHaveBeenCalledTimes(1);
expect(windowMock.navigator.msSaveBlob).toHaveBeenCalledTimes(1);
expect(windowMock.document.createElement).not.toHaveBeenCalled();
expect(msSaveBlob).toHaveBeenCalledTimes(1);
expect(createElement).not.toHaveBeenCalled();
});
it('makes use of download link API when available', () => {
const windowMock = createWindowMock(false);
const exporter = new ServersExporter(storageMock, windowMock, createCsvjsonMock());
const { document: { createElement, body } } = windowMock;
const { appendChild, removeChild } = body;
exporter.exportServers();
expect(storageMock.get).toHaveBeenCalledTimes(1);
expect(windowMock.document.createElement).toHaveBeenCalledTimes(1);
expect(windowMock.document.body.appendChild).toHaveBeenCalledTimes(1);
expect(windowMock.document.body.removeChild).toHaveBeenCalledTimes(1);
expect(createElement).toHaveBeenCalledTimes(1);
expect(appendChild).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 = {
getItem: jest.fn((key) => key === 'shlink.foo' ? JSON.stringify({ foo: 'bar' }) : null),
setItem: jest.fn(),
@ -11,7 +11,7 @@ describe('Storage', () => {
localStorageMock.getItem.mockClear();
localStorageMock.setItem.mockReset();
storage = new Storage(localStorageMock);
storage = new LocalStorage(localStorageMock);
});
describe('set', () => {