@aerogel/core 0.0.0-next.b18f4e0acd39431045c2f444c711303890143193 → 0.0.0-next.b3caf219a503ce9b8c65ef1463132c9507f56c0a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aerogel-core.d.ts +1516 -1471
- package/dist/aerogel-core.js +2960 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +27 -37
- package/src/bootstrap/bootstrap.test.ts +4 -8
- package/src/bootstrap/index.ts +26 -16
- package/src/bootstrap/options.ts +1 -1
- package/src/components/{AGAppLayout.vue → AppLayout.vue} +4 -4
- package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
- package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +5 -5
- package/src/components/{AGAppSnackbars.vue → AppSnackbars.vue} +1 -1
- package/src/components/composition.ts +23 -0
- package/src/components/contracts/AlertModal.ts +4 -0
- package/src/components/contracts/Button.ts +15 -0
- package/src/components/contracts/ConfirmModal.ts +41 -0
- package/src/components/contracts/ErrorReportModal.ts +29 -0
- package/src/components/contracts/Input.ts +26 -0
- package/src/components/contracts/LoadingModal.ts +18 -0
- package/src/components/contracts/Modal.ts +9 -0
- package/src/components/contracts/PromptModal.ts +28 -0
- package/src/components/contracts/index.ts +7 -0
- package/src/components/contracts/shared.ts +9 -0
- package/src/components/forms/AGSelect.vue +11 -17
- package/src/components/forms/index.ts +0 -4
- package/src/components/headless/HeadlessButton.vue +45 -0
- package/src/components/headless/HeadlessInput.vue +59 -0
- package/src/components/headless/HeadlessInputDescription.vue +27 -0
- package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
- package/src/components/headless/HeadlessInputInput.vue +75 -0
- package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
- package/src/components/headless/HeadlessInputTextArea.vue +40 -0
- package/src/components/headless/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +16 -18
- package/src/components/headless/HeadlessModalContent.vue +24 -0
- package/src/components/headless/HeadlessModalOverlay.vue +12 -0
- package/src/components/headless/HeadlessModalTitle.vue +12 -0
- package/src/components/headless/forms/AGHeadlessSelect.ts +3 -3
- package/src/components/headless/forms/AGHeadlessSelect.vue +16 -16
- package/src/components/headless/forms/AGHeadlessSelectError.vue +2 -2
- package/src/components/headless/forms/AGHeadlessSelectOption.vue +10 -18
- package/src/components/headless/forms/AGHeadlessSelectOptions.vue +19 -0
- package/src/components/headless/forms/AGHeadlessSelectTrigger.vue +25 -0
- package/src/components/headless/forms/composition.ts +10 -0
- package/src/components/headless/forms/index.ts +3 -9
- package/src/components/headless/index.ts +12 -1
- package/src/components/headless/snackbars/index.ts +3 -3
- package/src/components/index.ts +6 -4
- package/src/components/lib/AGErrorMessage.vue +4 -4
- package/src/components/lib/AGMarkdown.vue +24 -6
- package/src/components/lib/AGMeasured.vue +3 -2
- package/src/components/lib/AGStartupCrash.vue +6 -6
- package/src/components/lib/index.ts +0 -1
- package/src/components/snackbars/AGSnackbar.vue +8 -6
- package/src/components/ui/AlertModal.vue +13 -0
- package/src/components/ui/Button.vue +58 -0
- package/src/components/ui/Checkbox.vue +49 -0
- package/src/components/ui/ConfirmModal.vue +42 -0
- package/src/components/ui/ErrorReportModal.vue +62 -0
- package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +29 -20
- package/src/components/ui/ErrorReportModalTitle.vue +24 -0
- package/src/components/ui/Form.vue +24 -0
- package/src/components/ui/Input.vue +52 -0
- package/src/components/ui/Link.vue +12 -0
- package/src/components/ui/LoadingModal.vue +32 -0
- package/src/components/ui/Modal.vue +55 -0
- package/src/components/ui/ModalContext.vue +30 -0
- package/src/components/ui/ProgressBar.vue +50 -0
- package/src/components/ui/PromptModal.vue +35 -0
- package/src/components/ui/index.ts +15 -0
- package/src/components/utils.ts +106 -9
- package/src/directives/index.ts +11 -5
- package/src/directives/measure.ts +34 -6
- package/src/errors/Errors.state.ts +1 -1
- package/src/errors/Errors.ts +25 -28
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +10 -16
- package/src/errors/utils.ts +35 -0
- package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
- package/src/forms/{Form.ts → FormController.ts} +85 -25
- package/src/forms/composition.ts +4 -4
- package/src/forms/index.ts +3 -1
- package/src/forms/utils.ts +36 -5
- package/src/forms/validation.ts +19 -0
- package/src/index.css +8 -0
- package/src/{main.ts → index.ts} +3 -0
- package/src/jobs/Job.ts +147 -0
- package/src/jobs/index.ts +10 -0
- package/src/jobs/listeners.ts +3 -0
- package/src/jobs/status.ts +4 -0
- package/src/lang/DefaultLangProvider.ts +46 -0
- package/src/lang/Lang.state.ts +11 -0
- package/src/lang/Lang.ts +44 -29
- package/src/lang/index.ts +8 -6
- package/src/plugins/Plugin.ts +1 -1
- package/src/plugins/index.ts +10 -7
- package/src/services/App.state.ts +27 -4
- package/src/services/App.ts +12 -4
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.test.ts +39 -0
- package/src/services/Events.ts +112 -32
- package/src/services/Service.ts +150 -55
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +14 -5
- package/src/services/store.ts +8 -5
- package/src/services/utils.ts +18 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/setup.ts +11 -0
- package/src/ui/UI.state.ts +17 -5
- package/src/ui/UI.ts +176 -60
- package/src/ui/index.ts +17 -16
- package/src/ui/utils.ts +16 -0
- package/src/utils/composition/events.ts +2 -2
- package/src/utils/composition/forms.ts +4 -3
- package/src/utils/composition/persistent.test.ts +33 -0
- package/src/utils/composition/persistent.ts +11 -0
- package/src/utils/composition/state.test.ts +47 -0
- package/src/utils/composition/state.ts +24 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/markdown.test.ts +50 -0
- package/src/utils/markdown.ts +19 -6
- package/src/utils/vue.ts +22 -15
- package/dist/aerogel-core.cjs.js +0 -2
- package/dist/aerogel-core.cjs.js.map +0 -1
- package/dist/aerogel-core.esm.js +0 -2
- package/dist/aerogel-core.esm.js.map +0 -1
- package/histoire.config.ts +0 -7
- package/noeldemartin.config.js +0 -5
- package/postcss.config.js +0 -6
- package/src/assets/histoire.css +0 -3
- package/src/components/forms/AGButton.vue +0 -44
- package/src/components/forms/AGCheckbox.vue +0 -41
- package/src/components/forms/AGForm.vue +0 -26
- package/src/components/forms/AGInput.vue +0 -38
- package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
- package/src/components/headless/forms/AGHeadlessInput.ts +0 -28
- package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
- package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
- package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
- package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
- package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
- package/src/components/headless/modals/index.ts +0 -4
- package/src/components/lib/AGLink.vue +0 -9
- package/src/components/modals/AGAlertModal.ts +0 -15
- package/src/components/modals/AGAlertModal.vue +0 -14
- package/src/components/modals/AGConfirmModal.ts +0 -27
- package/src/components/modals/AGConfirmModal.vue +0 -26
- package/src/components/modals/AGErrorReportModal.ts +0 -46
- package/src/components/modals/AGErrorReportModal.vue +0 -54
- package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
- package/src/components/modals/AGLoadingModal.ts +0 -23
- package/src/components/modals/AGLoadingModal.vue +0 -15
- package/src/components/modals/AGModal.ts +0 -10
- package/src/components/modals/AGModal.vue +0 -39
- package/src/components/modals/AGModalContext.ts +0 -8
- package/src/components/modals/AGModalContext.vue +0 -22
- package/src/components/modals/AGModalTitle.vue +0 -9
- package/src/components/modals/AGPromptModal.ts +0 -30
- package/src/components/modals/AGPromptModal.vue +0 -34
- package/src/components/modals/index.ts +0 -17
- package/src/directives/initial-focus.ts +0 -11
- package/src/main.histoire.ts +0 -1
- package/tailwind.config.js +0 -4
- package/tsconfig.json +0 -11
- package/vite.config.ts +0 -14
package/src/services/Service.ts
CHANGED
|
@@ -1,69 +1,114 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
MagicObject,
|
|
3
|
+
PromisedValue,
|
|
4
|
+
Storage,
|
|
5
|
+
arrayFrom,
|
|
6
|
+
fail,
|
|
7
|
+
isEmpty,
|
|
8
|
+
objectDeepClone,
|
|
9
|
+
objectOnly,
|
|
10
|
+
} from '@noeldemartin/utils';
|
|
11
|
+
import type { Constructor, Nullable } from '@noeldemartin/utils';
|
|
4
12
|
import type { Store } from 'pinia';
|
|
5
13
|
|
|
6
|
-
import ServiceBootError from '
|
|
7
|
-
import { defineServiceStore } from '
|
|
14
|
+
import ServiceBootError from '@aerogel/core/errors/ServiceBootError';
|
|
15
|
+
import { defineServiceStore } from '@aerogel/core/services/store';
|
|
16
|
+
import type { Unref } from '@aerogel/core/utils/vue';
|
|
8
17
|
|
|
9
18
|
export type ServiceState = Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
10
19
|
export type DefaultServiceState = any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
11
20
|
export type ServiceConstructor<T extends Service = Service> = Constructor<T> & typeof Service;
|
|
12
|
-
export type UnrefServiceState<State extends ServiceState> = {
|
|
13
|
-
[K in keyof State]: State[K] extends MaybeRef<infer T> ? T : State[K];
|
|
14
|
-
};
|
|
15
21
|
|
|
16
22
|
export type ComputedStateDefinition<TState extends ServiceState, TComputedState extends ServiceState> = {
|
|
17
|
-
[K in keyof TComputedState]: (state:
|
|
23
|
+
[K in keyof TComputedState]: (state: Unref<TState>) => TComputedState[K];
|
|
18
24
|
} & ThisType<{
|
|
19
25
|
readonly [K in keyof TComputedState]: TComputedState[K];
|
|
20
26
|
}>;
|
|
21
27
|
|
|
28
|
+
export type StateWatchers<TService extends Service, TState extends ServiceState> = {
|
|
29
|
+
[K in keyof TState]?: (this: TService, value: TState[K], oldValue: TState[K]) => unknown;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ServiceWithState<
|
|
33
|
+
State extends ServiceState = ServiceState,
|
|
34
|
+
ComputedState extends ServiceState = {},
|
|
35
|
+
ServiceStorage = Partial<State>,
|
|
36
|
+
> = Constructor<Unref<State>> &
|
|
37
|
+
Constructor<ComputedState> &
|
|
38
|
+
Constructor<Service<Unref<State>, ComputedState, Unref<ServiceStorage>>>;
|
|
39
|
+
|
|
22
40
|
export function defineServiceState<
|
|
23
41
|
State extends ServiceState = ServiceState,
|
|
24
|
-
ComputedState extends ServiceState = {}
|
|
42
|
+
ComputedState extends ServiceState = {},
|
|
43
|
+
ServiceStorage = Partial<State>,
|
|
25
44
|
>(options: {
|
|
26
45
|
name: string;
|
|
27
|
-
initialState: State;
|
|
46
|
+
initialState: State | (() => State);
|
|
28
47
|
persist?: (keyof State)[];
|
|
48
|
+
watch?: StateWatchers<Service, State>;
|
|
29
49
|
computed?: ComputedStateDefinition<State, ComputedState>;
|
|
30
|
-
serialize?: (state: Partial<State>) =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return class extends Service<UnrefServiceState<State>, ComputedState> {
|
|
50
|
+
serialize?: (state: Partial<State>) => ServiceStorage;
|
|
51
|
+
restore?: (state: ServiceStorage) => Partial<State>;
|
|
52
|
+
}): ServiceWithState<State, ComputedState, ServiceStorage> {
|
|
53
|
+
return class extends Service<Unref<State>, ComputedState, ServiceStorage> {
|
|
35
54
|
|
|
36
|
-
public static persist = (options.persist as string[]) ?? [];
|
|
55
|
+
public static override persist = (options.persist as string[]) ?? [];
|
|
37
56
|
|
|
38
|
-
protected usesStore(): boolean {
|
|
57
|
+
protected override usesStore(): boolean {
|
|
39
58
|
return true;
|
|
40
59
|
}
|
|
41
60
|
|
|
42
|
-
protected getName(): string | null {
|
|
61
|
+
protected override getName(): string | null {
|
|
43
62
|
return options.name ?? null;
|
|
44
63
|
}
|
|
45
64
|
|
|
46
|
-
protected getInitialState():
|
|
47
|
-
|
|
65
|
+
protected override getInitialState(): Unref<State> {
|
|
66
|
+
if (typeof options.initialState === 'function') {
|
|
67
|
+
return options.initialState();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return Object.entries(options.initialState).reduce((state, [key, value]) => {
|
|
71
|
+
try {
|
|
72
|
+
value = structuredClone(value);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.warn(
|
|
76
|
+
`Could not clone '${key}' state from ${this.getName()} service, ` +
|
|
77
|
+
'this may cause problems if you\'re using multiple instances of the service ' +
|
|
78
|
+
'(for example, in unit tests).\n' +
|
|
79
|
+
'To fix this problem, declare your initialState as a function instead.',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
state[key as keyof State] = value;
|
|
84
|
+
|
|
85
|
+
return state;
|
|
86
|
+
}, {} as Unref<State>);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
protected override getComputedStateDefinition(): ComputedStateDefinition<Unref<State>, ComputedState> {
|
|
90
|
+
return (options.computed ?? {}) as ComputedStateDefinition<Unref<State>, ComputedState>;
|
|
48
91
|
}
|
|
49
92
|
|
|
50
|
-
protected
|
|
51
|
-
return (options.
|
|
93
|
+
protected override getStateWatchers(): StateWatchers<Service, Unref<State>> {
|
|
94
|
+
return (options.watch ?? {}) as StateWatchers<Service, Unref<State>>;
|
|
52
95
|
}
|
|
53
96
|
|
|
54
|
-
protected serializePersistedState(state: Partial<State>):
|
|
55
|
-
return options.serialize?.(state) ?? state;
|
|
97
|
+
protected override serializePersistedState(state: Partial<State>): ServiceStorage {
|
|
98
|
+
return options.serialize?.(state) ?? (state as ServiceStorage);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected override deserializePersistedState(state: ServiceStorage): Partial<State> {
|
|
102
|
+
return options.restore?.(state) ?? (state as Partial<State>);
|
|
56
103
|
}
|
|
57
104
|
|
|
58
|
-
} as unknown as
|
|
59
|
-
Constructor<ComputedState> &
|
|
60
|
-
Constructor<Service<UnrefServiceState<State>, ComputedState, Partial<UnrefServiceState<State>>>>;
|
|
105
|
+
} as unknown as ServiceWithState<State, ComputedState, ServiceStorage>;
|
|
61
106
|
}
|
|
62
107
|
|
|
63
108
|
export default class Service<
|
|
64
109
|
State extends ServiceState = DefaultServiceState,
|
|
65
110
|
ComputedState extends ServiceState = {},
|
|
66
|
-
ServiceStorage
|
|
111
|
+
ServiceStorage = Partial<State>,
|
|
67
112
|
> extends MagicObject {
|
|
68
113
|
|
|
69
114
|
public static persist: string[] = [];
|
|
@@ -71,7 +116,8 @@ export default class Service<
|
|
|
71
116
|
protected _name: string;
|
|
72
117
|
private _booted: PromisedValue<void>;
|
|
73
118
|
private _computedStateKeys: Set<keyof State>;
|
|
74
|
-
private
|
|
119
|
+
private _watchers: StateWatchers<Service, State>;
|
|
120
|
+
private _store: Store<string, State, ComputedState, {}> | false;
|
|
75
121
|
|
|
76
122
|
constructor() {
|
|
77
123
|
super();
|
|
@@ -81,6 +127,7 @@ export default class Service<
|
|
|
81
127
|
this._name = this.getName() ?? new.target.name;
|
|
82
128
|
this._booted = new PromisedValue();
|
|
83
129
|
this._computedStateKeys = new Set(Object.keys(getters));
|
|
130
|
+
this._watchers = this.getStateWatchers();
|
|
84
131
|
this._store =
|
|
85
132
|
this.usesStore() &&
|
|
86
133
|
defineServiceStore(this._name, {
|
|
@@ -95,6 +142,12 @@ export default class Service<
|
|
|
95
142
|
return this._booted;
|
|
96
143
|
}
|
|
97
144
|
|
|
145
|
+
public override static<T extends typeof Service>(): T;
|
|
146
|
+
public override static<T extends typeof Service, K extends keyof T>(property: K): T[K];
|
|
147
|
+
public override static<T extends typeof Service, K extends keyof T>(property?: K): T | T[K] {
|
|
148
|
+
return super.static<T, K>(property as K);
|
|
149
|
+
}
|
|
150
|
+
|
|
98
151
|
public launch(): Promise<void> {
|
|
99
152
|
const handleError = (error: unknown) => this._booted.reject(new ServiceBootError(this._name, error));
|
|
100
153
|
|
|
@@ -110,6 +163,10 @@ export default class Service<
|
|
|
110
163
|
return this._booted;
|
|
111
164
|
}
|
|
112
165
|
|
|
166
|
+
public hasPersistedState(): boolean {
|
|
167
|
+
return Storage.has(this._name);
|
|
168
|
+
}
|
|
169
|
+
|
|
113
170
|
public hasState<P extends keyof State>(property: P): boolean {
|
|
114
171
|
if (!this._store) {
|
|
115
172
|
return false;
|
|
@@ -125,10 +182,10 @@ export default class Service<
|
|
|
125
182
|
const store = this._store as any;
|
|
126
183
|
|
|
127
184
|
if (property) {
|
|
128
|
-
return store ? store[property] : undefined;
|
|
185
|
+
return store ? store[property] : (undefined as State[P]);
|
|
129
186
|
}
|
|
130
187
|
|
|
131
|
-
return store ? store : {};
|
|
188
|
+
return store ? store : ({} as State);
|
|
132
189
|
}
|
|
133
190
|
|
|
134
191
|
public setState<P extends keyof State>(property: P, value: State[P]): void;
|
|
@@ -138,16 +195,31 @@ export default class Service<
|
|
|
138
195
|
return;
|
|
139
196
|
}
|
|
140
197
|
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
198
|
+
const update = typeof stateOrProperty === 'string' ? { [stateOrProperty]: value } : stateOrProperty;
|
|
199
|
+
const old = objectOnly(this._store.$state as State, Object.keys(update));
|
|
200
|
+
|
|
201
|
+
Object.assign(this._store.$state, update);
|
|
202
|
+
this.onStateUpdated(update as Partial<State>, old as Partial<State>);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
public updatePersistedState<T extends keyof State>(key: T): void;
|
|
206
|
+
public updatePersistedState<T extends keyof State>(keys: T[]): void;
|
|
207
|
+
public updatePersistedState<T extends keyof State>(keyOrKeys: T | T[]): void {
|
|
208
|
+
if (!this._store) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const keys = arrayFrom(keyOrKeys) as Array<keyof State>;
|
|
213
|
+
const state = objectOnly(this._store.$state as State, keys);
|
|
144
214
|
|
|
145
|
-
|
|
215
|
+
if (isEmpty(state)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
146
218
|
|
|
147
|
-
this.
|
|
219
|
+
this.onPersistentStateUpdated(state);
|
|
148
220
|
}
|
|
149
221
|
|
|
150
|
-
protected __get(property: string): unknown {
|
|
222
|
+
protected override __get(property: string): unknown {
|
|
151
223
|
if (this.hasState(property)) {
|
|
152
224
|
return this.getState(property);
|
|
153
225
|
}
|
|
@@ -155,19 +227,29 @@ export default class Service<
|
|
|
155
227
|
return super.__get(property);
|
|
156
228
|
}
|
|
157
229
|
|
|
158
|
-
protected __set(property: string, value: unknown): void {
|
|
230
|
+
protected override __set(property: string, value: unknown): void {
|
|
159
231
|
this.setState({ [property]: value } as Partial<State>);
|
|
160
232
|
}
|
|
161
233
|
|
|
162
|
-
protected onStateUpdated(
|
|
163
|
-
|
|
164
|
-
const persist = (this.constructor as unknown as { persist: string[] }).persist;
|
|
165
|
-
const persisted = objectOnly(state, persist);
|
|
234
|
+
protected onStateUpdated(update: Partial<State>, old: Partial<State>): void {
|
|
235
|
+
const persisted = objectOnly(update, this.static('persist'));
|
|
166
236
|
|
|
167
|
-
if (isEmpty(persisted)) {
|
|
168
|
-
|
|
237
|
+
if (!isEmpty(persisted)) {
|
|
238
|
+
this.onPersistentStateUpdated(persisted as Partial<State>);
|
|
169
239
|
}
|
|
170
240
|
|
|
241
|
+
for (const property in update) {
|
|
242
|
+
const watcher = this._watchers[property] as Nullable<(value: unknown, oldValue: unknown) => unknown>;
|
|
243
|
+
|
|
244
|
+
if (!watcher || update[property] === old[property]) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
watcher.call(this, update[property], old[property]);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
protected onPersistentStateUpdated(persisted: Partial<State>): void {
|
|
171
253
|
const storage = Storage.get<ServiceStorage>(this._name);
|
|
172
254
|
|
|
173
255
|
if (!storage) {
|
|
@@ -196,34 +278,47 @@ export default class Service<
|
|
|
196
278
|
return {} as ComputedStateDefinition<State, ComputedState>;
|
|
197
279
|
}
|
|
198
280
|
|
|
199
|
-
protected
|
|
200
|
-
return
|
|
281
|
+
protected getStateWatchers(): StateWatchers<Service, State> {
|
|
282
|
+
return {};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
protected serializePersistedState(state: Partial<State>): ServiceStorage {
|
|
286
|
+
return state as ServiceStorage;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
protected deserializePersistedState(state: ServiceStorage): Partial<State> {
|
|
290
|
+
return state as Partial<State>;
|
|
201
291
|
}
|
|
202
292
|
|
|
203
293
|
protected async frameworkBoot(): Promise<void> {
|
|
204
|
-
this.
|
|
294
|
+
this.restorePersistedState();
|
|
205
295
|
}
|
|
206
296
|
|
|
207
297
|
protected async boot(): Promise<void> {
|
|
208
298
|
// Placeholder for overrides, don't place any functionality here.
|
|
209
299
|
}
|
|
210
300
|
|
|
211
|
-
protected
|
|
212
|
-
|
|
213
|
-
const persist = (this.constructor as unknown as { persist: string[] }).persist;
|
|
214
|
-
|
|
215
|
-
if (!this.usesStore() || isEmpty(persist)) {
|
|
301
|
+
protected restorePersistedState(): void {
|
|
302
|
+
if (!this.usesStore() || isEmpty(this.static('persist'))) {
|
|
216
303
|
return;
|
|
217
304
|
}
|
|
218
305
|
|
|
219
306
|
if (Storage.has(this._name)) {
|
|
220
307
|
const persisted = Storage.require<ServiceStorage>(this._name);
|
|
221
|
-
this.setState(persisted);
|
|
308
|
+
this.setState(this.deserializePersistedState(persisted));
|
|
222
309
|
|
|
223
310
|
return;
|
|
224
311
|
}
|
|
225
312
|
|
|
226
|
-
Storage.set(this._name, objectOnly(this.getState(), persist));
|
|
313
|
+
Storage.set(this._name, objectOnly(this.getState(), this.static('persist')));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
protected requireStore(): Store<string, State, ComputedState, {}> {
|
|
317
|
+
if (!this._store) {
|
|
318
|
+
return fail(`Failed getting '${this._name}' store`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return this._store;
|
|
227
322
|
}
|
|
228
323
|
|
|
229
324
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { facade } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
import Events from '@aerogel/core/services/Events';
|
|
4
|
+
import Service from '@aerogel/core/services/Service';
|
|
5
|
+
|
|
6
|
+
export class StorageService extends Service {
|
|
7
|
+
|
|
8
|
+
public async purge(): Promise<void> {
|
|
9
|
+
await Events.emit('purge-storage');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default facade(StorageService);
|
|
15
|
+
|
|
16
|
+
declare module '@aerogel/core/services/Events' {
|
|
17
|
+
export interface EventsPayload {
|
|
18
|
+
'purge-storage': void;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/services/index.ts
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import type { App as VueApp } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { definePlugin } from '
|
|
3
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
4
|
+
import { isDevelopment, isTesting } from '@noeldemartin/utils';
|
|
4
5
|
|
|
5
6
|
import App from './App';
|
|
7
|
+
import Cache from './Cache';
|
|
6
8
|
import Events from './Events';
|
|
7
9
|
import Service from './Service';
|
|
10
|
+
import Storage from './Storage';
|
|
8
11
|
import { getPiniaStore } from './store';
|
|
9
12
|
|
|
10
13
|
export * from './App';
|
|
14
|
+
export * from './Cache';
|
|
11
15
|
export * from './Events';
|
|
12
16
|
export * from './Service';
|
|
17
|
+
export * from './store';
|
|
18
|
+
export * from './utils';
|
|
13
19
|
|
|
14
|
-
export { App, Events, Service };
|
|
20
|
+
export { App, Cache, Events, Storage, Service };
|
|
15
21
|
|
|
16
22
|
const defaultServices = {
|
|
17
23
|
$app: App,
|
|
18
24
|
$events: Events,
|
|
25
|
+
$storage: Storage,
|
|
19
26
|
};
|
|
20
27
|
|
|
21
28
|
export type DefaultServices = typeof defaultServices;
|
|
@@ -33,7 +40,9 @@ export async function bootServices(app: VueApp, services: Record<string, Service
|
|
|
33
40
|
|
|
34
41
|
Object.assign(app.config.globalProperties, services);
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
if (isDevelopment() || isTesting()) {
|
|
44
|
+
Object.assign(globalThis, services);
|
|
45
|
+
}
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
export default definePlugin({
|
|
@@ -49,12 +58,12 @@ export default definePlugin({
|
|
|
49
58
|
},
|
|
50
59
|
});
|
|
51
60
|
|
|
52
|
-
declare module '
|
|
61
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
53
62
|
export interface AerogelOptions {
|
|
54
63
|
services?: Record<string, Service>;
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
declare module '
|
|
67
|
+
declare module 'vue' {
|
|
59
68
|
interface ComponentCustomProperties extends Services {}
|
|
60
69
|
}
|
package/src/services/store.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
import { tap } from '@noeldemartin/utils';
|
|
1
2
|
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
|
2
3
|
import type { DefineStoreOptions, Pinia, StateTree, Store, _GettersTree } from 'pinia';
|
|
3
4
|
|
|
4
5
|
let _store: Pinia | null = null;
|
|
5
6
|
|
|
6
7
|
function initializePiniaStore(): Pinia {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
return _store ?? resetPiniaStore();
|
|
9
|
+
}
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
export function resetPiniaStore(): Pinia {
|
|
12
|
+
return tap(createPinia(), (store) => {
|
|
13
|
+
_store = store;
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
setActivePinia(store);
|
|
16
|
+
});
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export function getPiniaStore(): Pinia {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { objectOnly } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
export type Replace<
|
|
4
|
+
TOriginal extends Record<string, unknown>,
|
|
5
|
+
TReplacements extends Partial<Record<keyof TOriginal, unknown>>,
|
|
6
|
+
> = {
|
|
7
|
+
[K in keyof TOriginal]: TReplacements extends Record<K, infer Replacement> ? Replacement : TOriginal[K];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function replaceExisting<
|
|
11
|
+
TOriginal extends Record<string, unknown>,
|
|
12
|
+
TReplacements extends Partial<Record<keyof TOriginal, unknown>>,
|
|
13
|
+
>(original: TOriginal, replacements: TReplacements): Replace<TOriginal, TReplacements> {
|
|
14
|
+
return {
|
|
15
|
+
...original,
|
|
16
|
+
...objectOnly(replacements, Object.keys(original)),
|
|
17
|
+
} as Replace<TOriginal, TReplacements>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isTesting } from '@noeldemartin/utils';
|
|
2
|
+
import type { GetClosureArgs } from '@noeldemartin/utils';
|
|
3
|
+
|
|
4
|
+
import Events from '@aerogel/core/services/Events';
|
|
5
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
6
|
+
|
|
7
|
+
export interface AerogelTestingRuntime {
|
|
8
|
+
on: (typeof Events)['on'];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default definePlugin({
|
|
12
|
+
async install() {
|
|
13
|
+
if (!isTesting()) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
globalThis.testingRuntime = {
|
|
18
|
+
on: ((...args: GetClosureArgs<(typeof Events)['on']>) => Events.on(...args)) as (typeof Events)['on'],
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
declare global {
|
|
24
|
+
// eslint-disable-next-line no-var
|
|
25
|
+
var testingRuntime: AerogelTestingRuntime | undefined;
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FakeLocalStorage } from '@noeldemartin/testing';
|
|
2
|
+
import { beforeEach, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
vi.mock('dompurify', async () => {
|
|
5
|
+
return { default: { sanitize: (html: string) => html } };
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
FakeLocalStorage.reset();
|
|
10
|
+
FakeLocalStorage.patchGlobal();
|
|
11
|
+
});
|
package/src/ui/UI.state.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Component } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { defineServiceState } from '
|
|
3
|
+
import { defineServiceState } from '@aerogel/core/services/Service';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import { Layouts, getCurrentLayout } from './utils';
|
|
6
|
+
|
|
7
|
+
export interface UIModal<T = unknown> {
|
|
6
8
|
id: string;
|
|
7
9
|
properties: Record<string, unknown>;
|
|
8
10
|
component: Component;
|
|
@@ -10,11 +12,16 @@ export interface Modal<T = unknown> {
|
|
|
10
12
|
afterClose: Promise<T | undefined>;
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
export interface UIModalContext {
|
|
16
|
+
modal: UIModal;
|
|
17
|
+
childIndex?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
export interface ModalComponent<
|
|
14
21
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
15
|
-
Properties extends
|
|
22
|
+
Properties extends object = object,
|
|
16
23
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
17
|
-
Result = unknown
|
|
24
|
+
Result = unknown,
|
|
18
25
|
> {}
|
|
19
26
|
|
|
20
27
|
export interface Snackbar {
|
|
@@ -26,7 +33,12 @@ export interface Snackbar {
|
|
|
26
33
|
export default defineServiceState({
|
|
27
34
|
name: 'ui',
|
|
28
35
|
initialState: {
|
|
29
|
-
modals: [] as
|
|
36
|
+
modals: [] as UIModal[],
|
|
30
37
|
snackbars: [] as Snackbar[],
|
|
38
|
+
layout: getCurrentLayout(),
|
|
39
|
+
},
|
|
40
|
+
computed: {
|
|
41
|
+
mobile: ({ layout }) => layout === Layouts.Mobile,
|
|
42
|
+
desktop: ({ layout }) => layout === Layouts.Desktop,
|
|
31
43
|
},
|
|
32
44
|
});
|