mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-22 17:10:26 +03:00
Migrated all servers services to TS
This commit is contained in:
parent
aee4c2d02f
commit
64a968711c
12 changed files with 66 additions and 53 deletions
5
csvjson.d.ts
vendored
5
csvjson.d.ts
vendored
|
@ -1,5 +0,0 @@
|
|||
declare module 'csvjson' {
|
||||
export declare class CsvJson {
|
||||
public toObject<T>(content: string): T[];
|
||||
}
|
||||
}
|
7
shlink-web-client.d.ts
vendored
7
shlink-web-client.d.ts
vendored
|
@ -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'
|
||||
|
|
|
@ -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, {}),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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, {
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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' ]));
|
14
src/utils/services/LocalStorage.ts
Normal file
14
src/utils/services/LocalStorage.ts
Normal 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));
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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', () => {
|
||||
|
|
Loading…
Reference in a new issue