mirror of
https://github.com/gotify/server.git
synced 2026-06-02 08:24:44 +08:00
fix: MobX V6 actions (#906)
* fix: mobx action wrapping * migrate to decorator syntax
This commit is contained in:
@@ -2,7 +2,7 @@ import axios, {AxiosError, AxiosResponse} from 'axios';
|
||||
import * as config from './config';
|
||||
import {detect} from 'detect-browser';
|
||||
import {SnackReporter} from './snack/SnackManager';
|
||||
import {observable, makeObservable} from 'mobx';
|
||||
import {observable, runInAction, action} from 'mobx';
|
||||
import {IClient, IUser} from './types';
|
||||
|
||||
const tokenKey = 'gotify-login-key';
|
||||
@@ -11,21 +11,13 @@ export class CurrentUser {
|
||||
private tokenCache: string | null = null;
|
||||
private reconnectTimeoutId: number | null = null;
|
||||
private reconnectTime = 7500;
|
||||
public loggedIn = false;
|
||||
public refreshKey = 0;
|
||||
public authenticating = true;
|
||||
public user: IUser = {name: 'unknown', admin: false, id: -1};
|
||||
public connectionErrorMessage: string | null = null;
|
||||
@observable accessor loggedIn = false;
|
||||
@observable accessor refreshKey = 0;
|
||||
@observable accessor authenticating = true;
|
||||
@observable accessor user: IUser = {name: 'unknown', admin: false, id: -1};
|
||||
@observable accessor connectionErrorMessage: string | null = null;
|
||||
|
||||
public constructor(private readonly snack: SnackReporter) {
|
||||
makeObservable(this, {
|
||||
loggedIn: observable,
|
||||
authenticating: observable,
|
||||
user: observable,
|
||||
connectionErrorMessage: observable,
|
||||
refreshKey: observable,
|
||||
});
|
||||
}
|
||||
public constructor(private readonly snack: SnackReporter) {}
|
||||
|
||||
public token = (): string => {
|
||||
if (this.tokenCache !== null) {
|
||||
@@ -69,8 +61,10 @@ export class CurrentUser {
|
||||
});
|
||||
|
||||
public login = async (username: string, password: string) => {
|
||||
this.loggedIn = false;
|
||||
this.authenticating = true;
|
||||
runInAction(() => {
|
||||
this.loggedIn = false;
|
||||
this.authenticating = true;
|
||||
});
|
||||
const browser = detect();
|
||||
const name = (browser && browser.name + ' ' + browser.version) || 'unknown browser';
|
||||
axios
|
||||
@@ -90,50 +84,58 @@ export class CurrentUser {
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.authenticating = false;
|
||||
return this.snack('Login failed');
|
||||
});
|
||||
.catch(
|
||||
action(() => {
|
||||
this.authenticating = false;
|
||||
return this.snack('Login failed');
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
public tryAuthenticate = async (): Promise<AxiosResponse<IUser>> => {
|
||||
if (this.token() === '') {
|
||||
this.authenticating = false;
|
||||
runInAction(() => {
|
||||
this.authenticating = false;
|
||||
});
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return axios
|
||||
.create()
|
||||
.get(config.get('url') + 'current/user', {headers: {'X-Gotify-Key': this.token()}})
|
||||
.then((passThrough) => {
|
||||
this.user = passThrough.data;
|
||||
this.loggedIn = true;
|
||||
this.authenticating = false;
|
||||
this.connectionErrorMessage = null;
|
||||
this.reconnectTime = 7500;
|
||||
return passThrough;
|
||||
})
|
||||
.catch((error: AxiosError) => {
|
||||
this.authenticating = false;
|
||||
if (!error || !error.response) {
|
||||
this.connectionError('No network connection or server unavailable.');
|
||||
.then(
|
||||
action((passThrough) => {
|
||||
this.user = passThrough.data;
|
||||
this.loggedIn = true;
|
||||
this.authenticating = false;
|
||||
this.connectionErrorMessage = null;
|
||||
this.reconnectTime = 7500;
|
||||
return passThrough;
|
||||
})
|
||||
)
|
||||
.catch(
|
||||
action((error: AxiosError) => {
|
||||
this.authenticating = false;
|
||||
if (!error || !error.response) {
|
||||
this.connectionError('No network connection or server unavailable.');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (error.response.status >= 500) {
|
||||
this.connectionError(
|
||||
`${error.response.statusText} (code: ${error.response.status}).`
|
||||
);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
this.connectionErrorMessage = null;
|
||||
|
||||
if (error.response.status >= 400 && error.response.status < 500) {
|
||||
this.logout();
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (error.response.status >= 500) {
|
||||
this.connectionError(
|
||||
`${error.response.statusText} (code: ${error.response.status}).`
|
||||
);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
this.connectionErrorMessage = null;
|
||||
|
||||
if (error.response.status >= 400 && error.response.status < 500) {
|
||||
this.logout();
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
public logout = async () => {
|
||||
@@ -147,7 +149,9 @@ export class CurrentUser {
|
||||
.catch(() => Promise.resolve());
|
||||
window.localStorage.removeItem(tokenKey);
|
||||
this.tokenCache = null;
|
||||
this.loggedIn = false;
|
||||
runInAction(() => {
|
||||
this.loggedIn = false;
|
||||
});
|
||||
};
|
||||
|
||||
public changePassword = (pass: string) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {BaseStore} from '../common/BaseStore';
|
||||
import axios from 'axios';
|
||||
import * as config from '../config';
|
||||
import {action, makeObservable} from 'mobx';
|
||||
import {action} from 'mobx';
|
||||
import {SnackReporter} from '../snack/SnackManager';
|
||||
import {IApplication} from '../types';
|
||||
|
||||
@@ -10,12 +10,6 @@ export class AppStore extends BaseStore<IApplication> {
|
||||
|
||||
public constructor(private readonly snack: SnackReporter) {
|
||||
super();
|
||||
|
||||
makeObservable(this, {
|
||||
uploadImage: action,
|
||||
update: action,
|
||||
create: action,
|
||||
});
|
||||
}
|
||||
|
||||
protected requestItems = (): Promise<IApplication[]> =>
|
||||
@@ -29,6 +23,7 @@ export class AppStore extends BaseStore<IApplication> {
|
||||
return this.snack('Application deleted');
|
||||
});
|
||||
|
||||
@action
|
||||
public uploadImage = async (id: number, file: Blob): Promise<void> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
@@ -50,6 +45,7 @@ export class AppStore extends BaseStore<IApplication> {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
public update = async (
|
||||
id: number,
|
||||
name: string,
|
||||
@@ -65,6 +61,7 @@ export class AppStore extends BaseStore<IApplication> {
|
||||
this.snack('Application updated');
|
||||
};
|
||||
|
||||
@action
|
||||
public create = async (
|
||||
name: string,
|
||||
description: string,
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import {BaseStore} from '../common/BaseStore';
|
||||
import axios from 'axios';
|
||||
import * as config from '../config';
|
||||
import {action, makeObservable} from 'mobx';
|
||||
import {action} from 'mobx';
|
||||
import {SnackReporter} from '../snack/SnackManager';
|
||||
import {IClient} from '../types';
|
||||
|
||||
export class ClientStore extends BaseStore<IClient> {
|
||||
public constructor(private readonly snack: SnackReporter) {
|
||||
super();
|
||||
|
||||
makeObservable(this, {
|
||||
update: action,
|
||||
createNoNotifcation: action,
|
||||
create: action,
|
||||
});
|
||||
}
|
||||
|
||||
protected requestItems = (): Promise<IClient[]> =>
|
||||
@@ -25,18 +19,21 @@ export class ClientStore extends BaseStore<IClient> {
|
||||
.then(() => this.snack('Client deleted'));
|
||||
}
|
||||
|
||||
@action
|
||||
public update = async (id: number, name: string): Promise<void> => {
|
||||
await axios.put(`${config.get('url')}client/${id}`, {name});
|
||||
await this.refresh();
|
||||
this.snack('Client updated');
|
||||
};
|
||||
|
||||
@action
|
||||
public createNoNotifcation = async (name: string): Promise<IClient> => {
|
||||
const client = await axios.post(`${config.get('url')}client`, {name});
|
||||
await this.refresh();
|
||||
return client.data;
|
||||
};
|
||||
|
||||
@action
|
||||
public create = async (name: string): Promise<void> => {
|
||||
await this.createNoNotifcation(name);
|
||||
this.snack('Client added');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {action, observable, makeObservable} from 'mobx';
|
||||
import {action, observable} from 'mobx';
|
||||
|
||||
interface HasID {
|
||||
id: number;
|
||||
@@ -12,21 +12,27 @@ export interface IClearable {
|
||||
* Base implementation for handling items with ids.
|
||||
*/
|
||||
export abstract class BaseStore<T extends HasID> implements IClearable {
|
||||
protected items: T[] = [];
|
||||
@observable protected accessor items: T[] = [];
|
||||
|
||||
protected abstract requestItems(): Promise<T[]>;
|
||||
|
||||
protected abstract requestDelete(id: number): Promise<void>;
|
||||
|
||||
@action
|
||||
public remove = async (id: number): Promise<void> => {
|
||||
await this.requestDelete(id);
|
||||
await this.refresh();
|
||||
};
|
||||
|
||||
public refresh = async (): Promise<void> => {
|
||||
this.items = await this.requestItems().then((items) => items || []);
|
||||
};
|
||||
@action
|
||||
public refresh = (): Promise<void> =>
|
||||
this.requestItems().then(
|
||||
action((items) => {
|
||||
this.items = items || [];
|
||||
})
|
||||
);
|
||||
|
||||
@action
|
||||
public refreshIfMissing = async (id: number): Promise<void> => {
|
||||
if (this.getByIDOrUndefined(id) === undefined) {
|
||||
await this.refresh();
|
||||
@@ -46,18 +52,8 @@ export abstract class BaseStore<T extends HasID> implements IClearable {
|
||||
|
||||
public getItems = (): T[] => this.items;
|
||||
|
||||
@action
|
||||
public clear = (): void => {
|
||||
this.items = [];
|
||||
};
|
||||
|
||||
constructor() {
|
||||
// eslint-disable-next-line
|
||||
makeObservable<BaseStore<any>, 'items'>(this, {
|
||||
items: observable,
|
||||
remove: action,
|
||||
refresh: action,
|
||||
refreshIfMissing: action,
|
||||
clear: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {BaseStore} from '../common/BaseStore';
|
||||
import {action, IObservableArray, observable, reaction, makeObservable} from 'mobx';
|
||||
import {action, IObservableArray, observable, reaction, runInAction} from 'mobx';
|
||||
import axios, {AxiosResponse} from 'axios';
|
||||
import * as config from '../config';
|
||||
import {createTransformer} from 'mobx-utils';
|
||||
@@ -22,8 +22,8 @@ interface PendingDelete {
|
||||
}
|
||||
|
||||
export class MessagesStore {
|
||||
private state: Record<string, MessagesState> = {};
|
||||
private pendingDeletes: Map<number, PendingDelete> = observable.map();
|
||||
@observable private accessor state: Record<string, MessagesState> = {};
|
||||
@observable private accessor pendingDeletes: Map<number, PendingDelete> = observable.map();
|
||||
|
||||
private loading = false;
|
||||
|
||||
@@ -31,20 +31,6 @@ export class MessagesStore {
|
||||
private readonly appStore: BaseStore<IApplication>,
|
||||
private readonly snack: SnackReporter
|
||||
) {
|
||||
makeObservable<MessagesStore, 'state' | 'pendingDeletes'>(this, {
|
||||
state: observable,
|
||||
pendingDeletes: observable,
|
||||
addPendingDelete: action,
|
||||
executePendingDeletes: action,
|
||||
cancelPendingDelete: action,
|
||||
loadMore: action,
|
||||
publishSingleMessage: action,
|
||||
removeByApp: action,
|
||||
removeSingle: action,
|
||||
clearAll: action,
|
||||
refreshByApp: action,
|
||||
});
|
||||
|
||||
reaction(() => appStore.getItems(), this.createEmptyStatesForApps);
|
||||
}
|
||||
|
||||
@@ -59,6 +45,7 @@ export class MessagesStore {
|
||||
|
||||
public canLoadMore = (appId: number) => this.stateOf(appId, /*create*/ false).hasMore;
|
||||
|
||||
@action
|
||||
public loadMore = async (appId: number) => {
|
||||
const state = this.stateOf(appId);
|
||||
if (!state.hasMore || this.loading) {
|
||||
@@ -70,10 +57,12 @@ export class MessagesStore {
|
||||
const pagedResult = await this.fetchMessages(appId, state.nextSince).then(
|
||||
(resp) => resp.data
|
||||
);
|
||||
state.messages.replace([...state.messages, ...pagedResult.messages]);
|
||||
state.nextSince = pagedResult.paging.since ?? 0;
|
||||
state.hasMore = 'next' in pagedResult.paging;
|
||||
state.loaded = true;
|
||||
runInAction(() => {
|
||||
state.messages.replace([...state.messages, ...pagedResult.messages]);
|
||||
state.nextSince = pagedResult.paging.since ?? 0;
|
||||
state.hasMore = 'next' in pagedResult.paging;
|
||||
state.loaded = true;
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
@@ -81,6 +70,7 @@ export class MessagesStore {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
@action
|
||||
public publishSingleMessage = (message: IMessage) => {
|
||||
if (this.exists(AllMessages)) {
|
||||
this.stateOf(AllMessages).messages.unshift(message);
|
||||
@@ -90,6 +80,7 @@ export class MessagesStore {
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
public removeByApp = async (appId: number) => {
|
||||
if (appId === AllMessages) {
|
||||
await axios.delete(config.get('url') + 'message');
|
||||
@@ -104,9 +95,11 @@ export class MessagesStore {
|
||||
await this.loadMore(appId);
|
||||
};
|
||||
|
||||
@action
|
||||
public addPendingDelete = (pending: PendingDelete) =>
|
||||
this.pendingDeletes.set(pending.message.id, pending);
|
||||
|
||||
@action
|
||||
public cancelPendingDelete = (message: IMessage): boolean => {
|
||||
const pending = this.pendingDeletes.get(message.id);
|
||||
if (pending) {
|
||||
@@ -116,11 +109,13 @@ export class MessagesStore {
|
||||
return !!pending;
|
||||
};
|
||||
|
||||
@action
|
||||
public executePendingDeletes = () =>
|
||||
Array.from(this.pendingDeletes.values()).forEach(({message}) => this.removeSingle(message));
|
||||
|
||||
public visible = (message: number): boolean => !this.pendingDeletes.has(message);
|
||||
|
||||
@action
|
||||
public removeSingle = async (message: IMessage) => {
|
||||
if (!this.pendingDeletes.has(message.id)) {
|
||||
return;
|
||||
@@ -158,11 +153,13 @@ export class MessagesStore {
|
||||
this.snack(`Message sent to ${app.name}`);
|
||||
};
|
||||
|
||||
@action
|
||||
public clearAll = () => {
|
||||
this.state = {};
|
||||
this.createEmptyStatesForApps(this.appStore.getItems());
|
||||
};
|
||||
|
||||
@action
|
||||
public refreshByApp = async (appId: number) => {
|
||||
this.clearAll();
|
||||
this.loadMore(appId);
|
||||
@@ -170,6 +167,7 @@ export class MessagesStore {
|
||||
|
||||
public exists = (id: number) => this.stateOf(id).loaded;
|
||||
|
||||
@action
|
||||
private removeFromList(messages: IMessage[], messageToDelete: IMessage): false | number {
|
||||
if (messages) {
|
||||
const index = messages.findIndex((message) => message.id === messageToDelete.id);
|
||||
@@ -181,6 +179,7 @@ export class MessagesStore {
|
||||
return false;
|
||||
}
|
||||
|
||||
@action
|
||||
private clear = (appId: number) => (this.state[appId] = this.emptyState());
|
||||
|
||||
private fetchMessages = (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import {action, makeObservable} from 'mobx';
|
||||
import {action} from 'mobx';
|
||||
import {BaseStore} from '../common/BaseStore';
|
||||
import * as config from '../config';
|
||||
import {SnackReporter} from '../snack/SnackManager';
|
||||
@@ -10,11 +10,6 @@ export class PluginStore extends BaseStore<IPlugin> {
|
||||
|
||||
public constructor(private readonly snack: SnackReporter) {
|
||||
super();
|
||||
|
||||
makeObservable(this, {
|
||||
changeConfig: action,
|
||||
changeEnabledState: action,
|
||||
});
|
||||
}
|
||||
|
||||
public requestConfig = (id: number): Promise<string> =>
|
||||
@@ -36,6 +31,7 @@ export class PluginStore extends BaseStore<IPlugin> {
|
||||
return id === -1 ? 'All Plugins' : plugin !== undefined ? plugin.name : 'unknown';
|
||||
};
|
||||
|
||||
@action
|
||||
public changeConfig = async (id: number, newConfig: string): Promise<void> => {
|
||||
await axios.post(`${config.get('url')}plugin/${id}/config`, newConfig, {
|
||||
headers: {'content-type': 'application/x-yaml'},
|
||||
@@ -44,6 +40,7 @@ export class PluginStore extends BaseStore<IPlugin> {
|
||||
await this.refresh();
|
||||
};
|
||||
|
||||
@action
|
||||
public changeEnabledState = async (id: number, enabled: boolean): Promise<void> => {
|
||||
await axios.post(`${config.get('url')}plugin/${id}/${enabled ? 'enable' : 'disable'}`);
|
||||
this.snack(`Plugin ${enabled ? 'enabled' : 'disabled'}`);
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import {BaseStore} from '../common/BaseStore';
|
||||
import axios from 'axios';
|
||||
import * as config from '../config';
|
||||
import {action, makeObservable} from 'mobx';
|
||||
import {action} from 'mobx';
|
||||
import {SnackReporter} from '../snack/SnackManager';
|
||||
import {IUser} from '../types';
|
||||
|
||||
export class UserStore extends BaseStore<IUser> {
|
||||
constructor(private readonly snack: SnackReporter) {
|
||||
super();
|
||||
|
||||
makeObservable(this, {
|
||||
create: action,
|
||||
update: action,
|
||||
});
|
||||
}
|
||||
|
||||
protected requestItems = (): Promise<IUser[]> =>
|
||||
@@ -24,12 +19,14 @@ export class UserStore extends BaseStore<IUser> {
|
||||
.then(() => this.snack('User deleted'));
|
||||
}
|
||||
|
||||
@action
|
||||
public create = async (name: string, pass: string, admin: boolean) => {
|
||||
await axios.post(`${config.get('url')}user`, {name, pass, admin});
|
||||
await this.refresh();
|
||||
this.snack('User created');
|
||||
};
|
||||
|
||||
@action
|
||||
public update = async (id: number, name: string, pass: string | null, admin: boolean) => {
|
||||
await axios.post(config.get('url') + 'user/' + id, {name, pass, admin});
|
||||
await this.refresh();
|
||||
|
||||
Reference in New Issue
Block a user