diff --git a/ui/package.json b/ui/package.json index 10979cd..6235feb 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,9 +15,9 @@ "@vitejs/plugin-react": "^4.7.0", "axios": "^1.11.0", "detect-browser": "^5.3.0", - "mobx": "^5.15.6", - "mobx-react": "^6.3.0", - "mobx-utils": "^5.6.1", + "mobx": "^6.13.7", + "mobx-react": "^9.2.0", + "mobx-utils": "^6.1.1", "notifyjs": "^3.0.0", "notistack": "^3.0.2", "react": "^19.1.1", diff --git a/ui/src/CurrentUser.ts b/ui/src/CurrentUser.ts index 34e2e0a..39409cd 100644 --- a/ui/src/CurrentUser.ts +++ b/ui/src/CurrentUser.ts @@ -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} from 'mobx'; +import {observable, makeObservable} from 'mobx'; import {IClient, IUser} from './types'; const tokenKey = 'gotify-login-key'; @@ -11,16 +11,19 @@ export class CurrentUser { private tokenCache: string | null = null; private reconnectTimeoutId: number | null = null; private reconnectTime = 7500; - @observable public loggedIn = false; - @observable public authenticating = true; - @observable public user: IUser = {name: 'unknown', admin: false, id: -1}; - @observable public connectionErrorMessage: string | null = null; - public constructor(private readonly snack: SnackReporter) {} + public constructor(private readonly snack: SnackReporter) { + makeObservable(this, { + loggedIn: observable, + authenticating: observable, + user: observable, + connectionErrorMessage: observable, + }); + } public token = (): string => { if (this.tokenCache !== null) { diff --git a/ui/src/application/AppStore.ts b/ui/src/application/AppStore.ts index dc5d5f2..9ffacb3 100644 --- a/ui/src/application/AppStore.ts +++ b/ui/src/application/AppStore.ts @@ -1,7 +1,7 @@ import {BaseStore} from '../common/BaseStore'; import axios from 'axios'; import * as config from '../config'; -import {action} from 'mobx'; +import {action, makeObservable} from 'mobx'; import {SnackReporter} from '../snack/SnackManager'; import {IApplication} from '../types'; @@ -10,6 +10,12 @@ export class AppStore extends BaseStore { public constructor(private readonly snack: SnackReporter) { super(); + + makeObservable(this, { + uploadImage: action, + update: action, + create: action, + }); } protected requestItems = (): Promise => @@ -23,7 +29,6 @@ export class AppStore extends BaseStore { return this.snack('Application deleted'); }); - @action public uploadImage = async (id: number, file: Blob): Promise => { const formData = new FormData(); formData.append('file', file); @@ -34,7 +39,6 @@ export class AppStore extends BaseStore { this.snack('Application image updated'); }; - @action public update = async ( id: number, name: string, @@ -50,7 +54,6 @@ export class AppStore extends BaseStore { this.snack('Application updated'); }; - @action public create = async ( name: string, description: string, diff --git a/ui/src/client/ClientStore.ts b/ui/src/client/ClientStore.ts index cc63b6e..3e7edd7 100644 --- a/ui/src/client/ClientStore.ts +++ b/ui/src/client/ClientStore.ts @@ -1,13 +1,19 @@ import {BaseStore} from '../common/BaseStore'; import axios from 'axios'; import * as config from '../config'; -import {action} from 'mobx'; +import {action, makeObservable} from 'mobx'; import {SnackReporter} from '../snack/SnackManager'; import {IClient} from '../types'; export class ClientStore extends BaseStore { public constructor(private readonly snack: SnackReporter) { super(); + + makeObservable(this, { + update: action, + createNoNotifcation: action, + create: action, + }); } protected requestItems = (): Promise => @@ -19,21 +25,18 @@ export class ClientStore extends BaseStore { .then(() => this.snack('Client deleted')); } - @action public update = async (id: number, name: string): Promise => { await axios.put(`${config.get('url')}client/${id}`, {name}); await this.refresh(); this.snack('Client updated'); }; - @action public createNoNotifcation = async (name: string): Promise => { const client = await axios.post(`${config.get('url')}client`, {name}); await this.refresh(); return client.data; }; - @action public create = async (name: string): Promise => { await this.createNoNotifcation(name); this.snack('Client added'); diff --git a/ui/src/common/BaseStore.ts b/ui/src/common/BaseStore.ts index 4b2b4ee..7601400 100644 --- a/ui/src/common/BaseStore.ts +++ b/ui/src/common/BaseStore.ts @@ -1,4 +1,4 @@ -import {action, observable} from 'mobx'; +import {action, observable, makeObservable} from 'mobx'; interface HasID { id: number; @@ -12,25 +12,21 @@ export interface IClearable { * Base implementation for handling items with ids. */ export abstract class BaseStore implements IClearable { - @observable protected items: T[] = []; protected abstract requestItems(): Promise; protected abstract requestDelete(id: number): Promise; - @action public remove = async (id: number): Promise => { await this.requestDelete(id); await this.refresh(); }; - @action public refresh = async (): Promise => { this.items = await this.requestItems().then((items) => items || []); }; - @action public refreshIfMissing = async (id: number): Promise => { if (this.getByIDOrUndefined(id) === undefined) { await this.refresh(); @@ -50,8 +46,18 @@ export abstract class BaseStore implements IClearable { public getItems = (): T[] => this.items; - @action public clear = (): void => { this.items = []; }; + + constructor() { + // eslint-disable-next-line + makeObservable, 'items'>(this, { + items: observable, + remove: action, + refresh: action, + refreshIfMissing: action, + clear: action, + }); + } } diff --git a/ui/src/message/MessagesStore.ts b/ui/src/message/MessagesStore.ts index 24ff431..406fcb3 100644 --- a/ui/src/message/MessagesStore.ts +++ b/ui/src/message/MessagesStore.ts @@ -1,5 +1,5 @@ import {BaseStore} from '../common/BaseStore'; -import {action, IObservableArray, observable, reaction} from 'mobx'; +import {action, IObservableArray, observable, reaction, makeObservable} from 'mobx'; import axios, {AxiosResponse} from 'axios'; import * as config from '../config'; import {createTransformer} from 'mobx-utils'; @@ -16,7 +16,6 @@ interface MessagesState { } export class MessagesStore { - @observable private state: Record = {}; private loading = false; @@ -25,6 +24,16 @@ export class MessagesStore { private readonly appStore: BaseStore, private readonly snack: SnackReporter ) { + makeObservable(this, { + state: observable, + loadMore: action, + publishSingleMessage: action, + removeByApp: action, + removeSingle: action, + clearAll: action, + refreshByApp: action, + }); + reaction(() => appStore.getItems(), this.createEmptyStatesForApps); } @@ -39,7 +48,6 @@ 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) { @@ -59,7 +67,6 @@ export class MessagesStore { return Promise.resolve(); }; - @action public publishSingleMessage = (message: IMessage) => { if (this.exists(AllMessages)) { this.stateOf(AllMessages).messages.unshift(message); @@ -69,7 +76,6 @@ export class MessagesStore { } }; - @action public removeByApp = async (appId: number) => { if (appId === AllMessages) { await axios.delete(config.get('url') + 'message'); @@ -84,7 +90,6 @@ export class MessagesStore { await this.loadMore(appId); }; - @action public removeSingle = async (message: IMessage) => { await axios.delete(config.get('url') + 'message/' + message.id); if (this.exists(AllMessages)) { @@ -96,13 +101,11 @@ export class MessagesStore { this.snack('Message deleted'); }; - @action public clearAll = () => { this.state = {}; this.createEmptyStatesForApps(this.appStore.getItems()); }; - @action public refreshByApp = async (appId: number) => { this.clearAll(); this.loadMore(appId); diff --git a/ui/src/plugin/PluginStore.ts b/ui/src/plugin/PluginStore.ts index 363af47..920366b 100644 --- a/ui/src/plugin/PluginStore.ts +++ b/ui/src/plugin/PluginStore.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import {action} from 'mobx'; +import {action, makeObservable} from 'mobx'; import {BaseStore} from '../common/BaseStore'; import * as config from '../config'; import {SnackReporter} from '../snack/SnackManager'; @@ -10,6 +10,11 @@ export class PluginStore extends BaseStore { public constructor(private readonly snack: SnackReporter) { super(); + + makeObservable(this, { + changeConfig: action, + changeEnabledState: action, + }); } public requestConfig = (id: number): Promise => @@ -31,7 +36,6 @@ export class PluginStore extends BaseStore { return id === -1 ? 'All Plugins' : plugin !== undefined ? plugin.name : 'unknown'; }; - @action public changeConfig = async (id: number, newConfig: string): Promise => { await axios.post(`${config.get('url')}plugin/${id}/config`, newConfig, { headers: {'content-type': 'application/x-yaml'}, @@ -40,7 +44,6 @@ export class PluginStore extends BaseStore { await this.refresh(); }; - @action public changeEnabledState = async (id: number, enabled: boolean): Promise => { await axios.post(`${config.get('url')}plugin/${id}/${enabled ? 'enable' : 'disable'}`); this.snack(`Plugin ${enabled ? 'enabled' : 'disabled'}`); diff --git a/ui/src/user/UserStore.ts b/ui/src/user/UserStore.ts index dc42f37..5068b69 100644 --- a/ui/src/user/UserStore.ts +++ b/ui/src/user/UserStore.ts @@ -1,13 +1,18 @@ import {BaseStore} from '../common/BaseStore'; import axios from 'axios'; import * as config from '../config'; -import {action} from 'mobx'; +import {action, makeObservable} from 'mobx'; import {SnackReporter} from '../snack/SnackManager'; import {IUser} from '../types'; export class UserStore extends BaseStore { constructor(private readonly snack: SnackReporter) { super(); + + makeObservable(this, { + create: action, + update: action, + }); } protected requestItems = (): Promise => @@ -19,14 +24,12 @@ export class UserStore extends BaseStore { .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(); diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 892284f..d5e4e10 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -16,7 +16,6 @@ "strictNullChecks": true, "noUnusedLocals": true, "allowSyntheticDefaultImports": true, - "experimentalDecorators": true, "skipLibCheck": true, "esModuleInterop": true, "strict": true, diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 769069a..639f6cf 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -10,15 +10,7 @@ export default defineConfig({ sourcemap: false, assetsDir: 'static', }, - plugins: [ - react({ - babel: { - parserOpts: { - plugins: ['decorators-legacy'], - }, - }, - }), - ], + plugins: [react()], define: { // Some libraries use the global object, even though it doesn't exist in the browser. // Alternatively, we could add `` to index.html. diff --git a/ui/yarn.lock b/ui/yarn.lock index 6914a49..25677a2 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -3613,27 +3613,29 @@ mitt@^3.0.1: resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== -mobx-react-lite@^2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-2.2.2.tgz#87c217dc72b4e47b22493daf155daf3759f868a6" - integrity sha512-2SlXALHIkyUPDsV4VTKVR9DW7K3Ksh1aaIv3NrNJygTbhXe2A9GrcKHZ2ovIiOp/BXilOcTYemfHHZubP431dg== - -mobx-react@^6.3.0: - version "6.3.1" - resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-6.3.1.tgz#204f9756e42e19d91cb6598837063b7e7de87c52" - integrity sha512-IOxdJGnRSNSJrL2uGpWO5w9JH5q5HoxEqwOF4gye1gmZYdjoYkkMzSGMDnRCUpN/BNzZcFoMdHXrjvkwO7KgaQ== +mobx-react-lite@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-4.1.0.tgz#6a03ed2d94150848213cfebd7d172e123528a972" + integrity sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w== dependencies: - mobx-react-lite "^2.2.0" + use-sync-external-store "^1.4.0" -mobx-utils@^5.6.1: - version "5.6.2" - resolved "https://registry.yarnpkg.com/mobx-utils/-/mobx-utils-5.6.2.tgz#4858acbdb03f0470e260854f87e8c2ba916ebaec" - integrity sha512-a/WlXyGkp6F12b01sTarENpxbmlRgPHFyR1Xv2bsSjQBm5dcOtd16ONb40/vOqck8L99NHpI+C9MXQ+SZ8f+yw== +mobx-react@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-9.2.0.tgz#c1e4d1ed406f6664d9de0787c948bac3a7ed5893" + integrity sha512-dkGWCx+S0/1mfiuFfHRH8D9cplmwhxOV5CkXMp38u6rQGG2Pv3FWYztS0M7ncR6TyPRQKaTG/pnitInoYE9Vrw== + dependencies: + mobx-react-lite "^4.1.0" -mobx@^5.15.6: - version "5.15.7" - resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.7.tgz#b9a5f2b6251f5d96980d13c78e9b5d8d4ce22665" - integrity sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw== +mobx-utils@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/mobx-utils/-/mobx-utils-6.1.1.tgz#61c66563e7646fb75462c189f4110a76d2e35768" + integrity sha512-ZR4tOKucWAHOdMjqElRl2BEvrzK7duuDdKmsbEbt2kzgVpuLuoYLiDCjc3QwWQl8CmOlxPgaZQpZ7emwNqPkIg== + +mobx@^6.13.7: + version "6.13.7" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.13.7.tgz#70e5dda7a45da947f773b3cd3b065dfe7c8a75de" + integrity sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g== ms@^2.1.3: version "2.1.3" @@ -4543,6 +4545,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-sync-external-store@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0" + integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== + vfile-message@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4"