@aerogel/core 0.0.0-next.f16bd1d894543c5303039c49f6f33488a1ffe931 → 0.0.0-next.f86b4b09f066c4aef21796a37dbc8417b7dce3cd
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.cjs.js +1 -1
- package/dist/aerogel-core.cjs.js.map +1 -1
- package/dist/aerogel-core.d.ts +1396 -235
- package/dist/aerogel-core.esm.js +1 -1
- package/dist/aerogel-core.esm.js.map +1 -1
- package/histoire.config.ts +7 -0
- package/package.json +14 -5
- package/postcss.config.js +6 -0
- package/src/assets/histoire.css +3 -0
- package/src/bootstrap/bootstrap.test.ts +3 -3
- package/src/bootstrap/index.ts +35 -5
- package/src/bootstrap/options.ts +3 -0
- package/src/components/AGAppLayout.vue +7 -2
- package/src/components/AGAppOverlays.vue +5 -1
- package/src/components/AGAppSnackbars.vue +2 -2
- package/src/components/composition.ts +23 -0
- package/src/components/forms/AGCheckbox.vue +7 -1
- package/src/components/forms/AGForm.vue +9 -10
- package/src/components/forms/AGInput.vue +10 -6
- package/src/components/forms/AGSelect.story.vue +46 -0
- package/src/components/forms/AGSelect.vue +60 -0
- package/src/components/forms/index.ts +5 -6
- package/src/components/headless/forms/AGHeadlessButton.ts +3 -0
- package/src/components/headless/forms/AGHeadlessButton.vue +24 -12
- package/src/components/headless/forms/AGHeadlessInput.ts +30 -4
- package/src/components/headless/forms/AGHeadlessInput.vue +23 -7
- package/src/components/headless/forms/AGHeadlessInputDescription.vue +28 -0
- package/src/components/headless/forms/AGHeadlessInputInput.vue +44 -5
- package/src/components/headless/forms/AGHeadlessInputLabel.vue +8 -2
- package/src/components/headless/forms/AGHeadlessInputTextArea.vue +43 -0
- package/src/components/headless/forms/AGHeadlessSelect.ts +42 -0
- package/src/components/headless/forms/AGHeadlessSelect.vue +77 -0
- package/src/components/headless/forms/AGHeadlessSelectButton.vue +24 -0
- package/src/components/headless/forms/AGHeadlessSelectError.vue +26 -0
- package/src/components/headless/forms/AGHeadlessSelectLabel.vue +24 -0
- package/src/components/headless/forms/AGHeadlessSelectOption.ts +4 -0
- package/src/components/headless/forms/AGHeadlessSelectOption.vue +39 -0
- package/src/components/headless/forms/AGHeadlessSelectOptions.ts +3 -0
- package/src/components/headless/forms/composition.ts +10 -0
- package/src/components/headless/forms/index.ts +13 -1
- package/src/components/headless/modals/AGHeadlessModal.ts +27 -0
- package/src/components/headless/modals/AGHeadlessModal.vue +3 -5
- package/src/components/headless/modals/index.ts +4 -6
- package/src/components/headless/snackbars/index.ts +23 -8
- package/src/components/index.ts +3 -1
- package/src/components/interfaces.ts +24 -0
- package/src/components/lib/AGErrorMessage.vue +16 -0
- package/src/components/lib/AGLink.vue +9 -0
- package/src/components/{basic → lib}/AGMarkdown.vue +12 -11
- package/src/components/lib/AGMeasured.vue +16 -0
- package/src/components/lib/AGStartupCrash.vue +31 -0
- package/src/components/lib/index.ts +5 -0
- package/src/components/modals/AGAlertModal.ts +15 -0
- package/src/components/modals/AGAlertModal.vue +4 -16
- package/src/components/modals/AGConfirmModal.ts +35 -0
- package/src/components/modals/AGConfirmModal.vue +9 -13
- package/src/components/modals/AGErrorReportModal.ts +27 -1
- package/src/components/modals/AGErrorReportModal.vue +8 -16
- package/src/components/modals/AGErrorReportModalButtons.vue +6 -1
- package/src/components/modals/AGErrorReportModalTitle.vue +2 -2
- package/src/components/modals/AGLoadingModal.ts +23 -0
- package/src/components/modals/AGLoadingModal.vue +4 -8
- package/src/components/modals/AGModal.ts +1 -1
- package/src/components/modals/AGModal.vue +15 -12
- package/src/components/modals/AGModalTitle.vue +9 -0
- package/src/components/modals/AGPromptModal.ts +36 -0
- package/src/components/modals/AGPromptModal.vue +34 -0
- package/src/components/modals/index.ts +13 -17
- package/src/components/snackbars/AGSnackbar.vue +4 -10
- package/src/components/utils.ts +10 -0
- package/src/directives/index.ts +5 -1
- package/src/directives/measure.ts +40 -0
- package/src/errors/Errors.ts +36 -12
- package/src/errors/index.ts +10 -23
- package/src/errors/utils.ts +35 -0
- package/src/forms/Form.test.ts +28 -0
- package/src/forms/Form.ts +80 -14
- package/src/forms/index.ts +3 -1
- package/src/forms/utils.ts +34 -3
- package/src/forms/validation.ts +19 -0
- package/src/jobs/Job.ts +5 -0
- package/src/jobs/index.ts +7 -0
- package/src/lang/DefaultLangProvider.ts +43 -0
- package/src/lang/Lang.state.ts +11 -0
- package/src/lang/Lang.ts +44 -29
- package/src/main.histoire.ts +1 -0
- package/src/main.ts +3 -0
- package/src/plugins/Plugin.ts +1 -0
- package/src/plugins/index.ts +19 -0
- package/src/services/App.state.ts +24 -6
- package/src/services/App.ts +43 -5
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.test.ts +39 -0
- package/src/services/Events.ts +100 -30
- package/src/services/Service.ts +64 -17
- package/src/services/index.ts +11 -5
- package/src/services/store.ts +8 -5
- package/src/testing/index.ts +25 -0
- package/src/testing/setup.ts +19 -0
- package/src/ui/UI.state.ts +7 -0
- package/src/ui/UI.ts +194 -27
- package/src/ui/index.ts +9 -3
- package/src/ui/utils.ts +16 -0
- package/src/utils/composition/events.ts +1 -0
- package/src/utils/composition/state.test.ts +47 -0
- package/src/utils/composition/state.ts +24 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/markdown.ts +11 -2
- package/src/utils/tailwindcss.test.ts +26 -0
- package/src/utils/tailwindcss.ts +7 -0
- package/src/utils/vue.ts +23 -5
- package/tailwind.config.js +4 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +4 -1
- package/.eslintrc.js +0 -3
- package/dist/virtual.d.ts +0 -11
- package/src/components/basic/index.ts +0 -3
- package/src/types/virtual.d.ts +0 -11
package/src/services/Service.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { MagicObject, PromisedValue, Storage, isEmpty, objectDeepClone, objectOnly } from '@noeldemartin/utils';
|
|
1
|
+
import { MagicObject, PromisedValue, Storage, fail, isEmpty, objectDeepClone, objectOnly } from '@noeldemartin/utils';
|
|
2
2
|
import type { Constructor } from '@noeldemartin/utils';
|
|
3
|
+
import type { MaybeRef } from 'vue';
|
|
3
4
|
import type { Store } from 'pinia';
|
|
4
5
|
|
|
5
6
|
import ServiceBootError from '@/errors/ServiceBootError';
|
|
@@ -8,9 +9,12 @@ import { defineServiceStore } from '@/services/store';
|
|
|
8
9
|
export type ServiceState = Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
9
10
|
export type DefaultServiceState = any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
10
11
|
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
|
+
};
|
|
11
15
|
|
|
12
16
|
export type ComputedStateDefinition<TState extends ServiceState, TComputedState extends ServiceState> = {
|
|
13
|
-
[K in keyof TComputedState]: (state: TState) => TComputedState[K];
|
|
17
|
+
[K in keyof TComputedState]: (state: UnrefServiceState<TState>) => TComputedState[K];
|
|
14
18
|
} & ThisType<{
|
|
15
19
|
readonly [K in keyof TComputedState]: TComputedState[K];
|
|
16
20
|
}>;
|
|
@@ -20,12 +24,14 @@ export function defineServiceState<
|
|
|
20
24
|
ComputedState extends ServiceState = {}
|
|
21
25
|
>(options: {
|
|
22
26
|
name: string;
|
|
23
|
-
initialState: State;
|
|
27
|
+
initialState: State | (() => State);
|
|
24
28
|
persist?: (keyof State)[];
|
|
25
29
|
computed?: ComputedStateDefinition<State, ComputedState>;
|
|
26
30
|
serialize?: (state: Partial<State>) => Partial<State>;
|
|
27
|
-
}): Constructor<State
|
|
28
|
-
|
|
31
|
+
}): Constructor<UnrefServiceState<State>> &
|
|
32
|
+
Constructor<ComputedState> &
|
|
33
|
+
Constructor<Service<UnrefServiceState<State>, ComputedState, Partial<UnrefServiceState<State>>>> {
|
|
34
|
+
return class extends Service<UnrefServiceState<State>, ComputedState> {
|
|
29
35
|
|
|
30
36
|
public static persist = (options.persist as string[]) ?? [];
|
|
31
37
|
|
|
@@ -37,21 +43,41 @@ export function defineServiceState<
|
|
|
37
43
|
return options.name ?? null;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
protected getInitialState(): State {
|
|
41
|
-
|
|
46
|
+
protected getInitialState(): UnrefServiceState<State> {
|
|
47
|
+
if (typeof options.initialState === 'function') {
|
|
48
|
+
return options.initialState();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return Object.entries(options.initialState).reduce((state, [key, value]) => {
|
|
52
|
+
try {
|
|
53
|
+
value = structuredClone(value);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// eslint-disable-next-line no-console
|
|
56
|
+
console.warn(
|
|
57
|
+
`Could not clone '${key}' state from ${this.getName()} service, ` +
|
|
58
|
+
'this may cause problems if you\'re using multiple instances of the service ' +
|
|
59
|
+
'(for example, in unit tests).\n' +
|
|
60
|
+
'To fix this problem, declare your initialState as a function instead.',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
state[key as keyof State] = value;
|
|
65
|
+
|
|
66
|
+
return state;
|
|
67
|
+
}, {} as UnrefServiceState<State>);
|
|
42
68
|
}
|
|
43
69
|
|
|
44
|
-
protected getComputedStateDefinition(): ComputedStateDefinition<State
|
|
45
|
-
return options.computed ??
|
|
70
|
+
protected getComputedStateDefinition(): ComputedStateDefinition<UnrefServiceState<State>, ComputedState> {
|
|
71
|
+
return (options.computed ?? {}) as ComputedStateDefinition<UnrefServiceState<State>, ComputedState>;
|
|
46
72
|
}
|
|
47
73
|
|
|
48
74
|
protected serializePersistedState(state: Partial<State>): Partial<State> {
|
|
49
75
|
return options.serialize?.(state) ?? state;
|
|
50
76
|
}
|
|
51
|
-
|
|
52
|
-
} as unknown as Constructor<State
|
|
77
|
+
|
|
78
|
+
} as unknown as Constructor<UnrefServiceState<State>> &
|
|
53
79
|
Constructor<ComputedState> &
|
|
54
|
-
Constructor<Service<State
|
|
80
|
+
Constructor<Service<UnrefServiceState<State>, ComputedState, Partial<UnrefServiceState<State>>>>;
|
|
55
81
|
}
|
|
56
82
|
|
|
57
83
|
export default class Service<
|
|
@@ -65,7 +91,7 @@ export default class Service<
|
|
|
65
91
|
protected _name: string;
|
|
66
92
|
private _booted: PromisedValue<void>;
|
|
67
93
|
private _computedStateKeys: Set<keyof State>;
|
|
68
|
-
private _store
|
|
94
|
+
private _store: Store<string, State, ComputedState, {}> | false;
|
|
69
95
|
|
|
70
96
|
constructor() {
|
|
71
97
|
super();
|
|
@@ -93,7 +119,8 @@ export default class Service<
|
|
|
93
119
|
const handleError = (error: unknown) => this._booted.reject(new ServiceBootError(this._name, error));
|
|
94
120
|
|
|
95
121
|
try {
|
|
96
|
-
this.
|
|
122
|
+
this.frameworkBoot()
|
|
123
|
+
.then(() => this.boot())
|
|
97
124
|
.then(() => this._booted.resolve())
|
|
98
125
|
.catch(handleError);
|
|
99
126
|
} catch (error) {
|
|
@@ -103,6 +130,10 @@ export default class Service<
|
|
|
103
130
|
return this._booted;
|
|
104
131
|
}
|
|
105
132
|
|
|
133
|
+
public hasPersistedState(): boolean {
|
|
134
|
+
return Storage.has(this._name);
|
|
135
|
+
}
|
|
136
|
+
|
|
106
137
|
public hasState<P extends keyof State>(property: P): boolean {
|
|
107
138
|
if (!this._store) {
|
|
108
139
|
return false;
|
|
@@ -161,7 +192,11 @@ export default class Service<
|
|
|
161
192
|
return;
|
|
162
193
|
}
|
|
163
194
|
|
|
164
|
-
const storage = Storage.
|
|
195
|
+
const storage = Storage.get<ServiceStorage>(this._name);
|
|
196
|
+
|
|
197
|
+
if (!storage) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
165
200
|
|
|
166
201
|
Storage.set(this._name, {
|
|
167
202
|
...storage,
|
|
@@ -189,11 +224,15 @@ export default class Service<
|
|
|
189
224
|
return state;
|
|
190
225
|
}
|
|
191
226
|
|
|
227
|
+
protected async frameworkBoot(): Promise<void> {
|
|
228
|
+
this.initializePersistedState();
|
|
229
|
+
}
|
|
230
|
+
|
|
192
231
|
protected async boot(): Promise<void> {
|
|
193
|
-
|
|
232
|
+
// Placeholder for overrides, don't place any functionality here.
|
|
194
233
|
}
|
|
195
234
|
|
|
196
|
-
protected
|
|
235
|
+
protected initializePersistedState(): void {
|
|
197
236
|
// TODO fix this.static()
|
|
198
237
|
const persist = (this.constructor as unknown as { persist: string[] }).persist;
|
|
199
238
|
|
|
@@ -211,4 +250,12 @@ export default class Service<
|
|
|
211
250
|
Storage.set(this._name, objectOnly(this.getState(), persist));
|
|
212
251
|
}
|
|
213
252
|
|
|
253
|
+
protected requireStore(): Store<string, State, ComputedState, {}> {
|
|
254
|
+
if (!this._store) {
|
|
255
|
+
return fail(`Failed getting '${this._name}' store`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return this._store;
|
|
259
|
+
}
|
|
260
|
+
|
|
214
261
|
}
|
package/src/services/index.ts
CHANGED
|
@@ -3,15 +3,18 @@ import type { App as VueApp } from 'vue';
|
|
|
3
3
|
import { definePlugin } from '@/plugins';
|
|
4
4
|
|
|
5
5
|
import App from './App';
|
|
6
|
+
import Cache from './Cache';
|
|
6
7
|
import Events from './Events';
|
|
7
8
|
import Service from './Service';
|
|
8
9
|
import { getPiniaStore } from './store';
|
|
9
10
|
|
|
10
11
|
export * from './App';
|
|
12
|
+
export * from './Cache';
|
|
11
13
|
export * from './Events';
|
|
12
14
|
export * from './Service';
|
|
15
|
+
export * from './store';
|
|
13
16
|
|
|
14
|
-
export { App, Events, Service };
|
|
17
|
+
export { App, Cache, Events, Service };
|
|
15
18
|
|
|
16
19
|
const defaultServices = {
|
|
17
20
|
$app: App,
|
|
@@ -24,13 +27,16 @@ export interface Services extends DefaultServices {}
|
|
|
24
27
|
|
|
25
28
|
export async function bootServices(app: VueApp, services: Record<string, Service>): Promise<void> {
|
|
26
29
|
await Promise.all(
|
|
27
|
-
Object.entries(services).map(async ([
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
Object.entries(services).map(async ([name, service]) => {
|
|
31
|
+
await service
|
|
32
|
+
.launch()
|
|
33
|
+
.catch((error) => app.config.errorHandler?.(error, null, `Failed launching ${name}.`));
|
|
30
34
|
}),
|
|
31
35
|
);
|
|
32
36
|
|
|
33
37
|
Object.assign(app.config.globalProperties, services);
|
|
38
|
+
|
|
39
|
+
App.development && Object.assign(window, services);
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
export default definePlugin({
|
|
@@ -47,7 +53,7 @@ export default definePlugin({
|
|
|
47
53
|
});
|
|
48
54
|
|
|
49
55
|
declare module '@/bootstrap/options' {
|
|
50
|
-
interface AerogelOptions {
|
|
56
|
+
export interface AerogelOptions {
|
|
51
57
|
services?: Record<string, Service>;
|
|
52
58
|
}
|
|
53
59
|
}
|
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,25 @@
|
|
|
1
|
+
import type { GetClosureArgs } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
import Events from '@/services/Events';
|
|
4
|
+
import { definePlugin } from '@/plugins';
|
|
5
|
+
|
|
6
|
+
export interface AerogelTestingRuntime {
|
|
7
|
+
on: (typeof Events)['on'];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default definePlugin({
|
|
11
|
+
async install() {
|
|
12
|
+
if (import.meta.env.MODE !== 'testing') {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
globalThis.testingRuntime = {
|
|
17
|
+
on: ((...args: GetClosureArgs<(typeof Events)['on']>) => Events.on(...args)) as (typeof Events)['on'],
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
declare global {
|
|
23
|
+
// eslint-disable-next-line no-var
|
|
24
|
+
var testingRuntime: AerogelTestingRuntime | undefined;
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { mock, tap } from '@noeldemartin/utils';
|
|
2
|
+
import { beforeEach, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
tap(globalThis, (global: any) => {
|
|
6
|
+
global.jest = vi;
|
|
7
|
+
global.navigator = { languages: ['en'] };
|
|
8
|
+
global.localStorage = mock<Storage>({
|
|
9
|
+
getItem: () => null,
|
|
10
|
+
setItem: () => null,
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.stubGlobal('document', {
|
|
16
|
+
querySelector: () => null,
|
|
17
|
+
getElementById: () => null,
|
|
18
|
+
});
|
|
19
|
+
});
|
package/src/ui/UI.state.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { Component } from 'vue';
|
|
|
2
2
|
|
|
3
3
|
import { defineServiceState } from '@/services/Service';
|
|
4
4
|
|
|
5
|
+
import { Layouts, getCurrentLayout } from './utils';
|
|
6
|
+
|
|
5
7
|
export interface Modal<T = unknown> {
|
|
6
8
|
id: string;
|
|
7
9
|
properties: Record<string, unknown>;
|
|
@@ -28,5 +30,10 @@ export default defineServiceState({
|
|
|
28
30
|
initialState: {
|
|
29
31
|
modals: [] as Modal[],
|
|
30
32
|
snackbars: [] as Snackbar[],
|
|
33
|
+
layout: getCurrentLayout(),
|
|
34
|
+
},
|
|
35
|
+
computed: {
|
|
36
|
+
mobile: ({ layout }) => layout === Layouts.Mobile,
|
|
37
|
+
desktop: ({ layout }) => layout === Layouts.Desktop,
|
|
31
38
|
},
|
|
32
39
|
});
|
package/src/ui/UI.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { facade, fail, uuid } from '@noeldemartin/utils';
|
|
1
|
+
import { after, facade, fail, uuid } from '@noeldemartin/utils';
|
|
2
2
|
import { markRaw, nextTick } from 'vue';
|
|
3
3
|
import type { Component } from 'vue';
|
|
4
4
|
import type { ObjectValues } from '@noeldemartin/utils';
|
|
5
5
|
|
|
6
|
+
import App from '@/services/App';
|
|
6
7
|
import Events from '@/services/Events';
|
|
8
|
+
import type { Color } from '@/components/constants';
|
|
7
9
|
import type { SnackbarAction, SnackbarColor } from '@/components/headless/snackbars';
|
|
10
|
+
import type { AGAlertModalProps, AGConfirmModalProps, AGLoadingModalProps, AGPromptModalProps } from '@/components';
|
|
8
11
|
|
|
9
12
|
import Service from './UI.state';
|
|
13
|
+
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
10
14
|
import type { Modal, ModalComponent, Snackbar } from './UI.state';
|
|
11
15
|
|
|
12
16
|
interface ModalCallbacks<T = unknown> {
|
|
@@ -24,11 +28,37 @@ export const UIComponents = {
|
|
|
24
28
|
ConfirmModal: 'confirm-modal',
|
|
25
29
|
ErrorReportModal: 'error-report-modal',
|
|
26
30
|
LoadingModal: 'loading-modal',
|
|
31
|
+
PromptModal: 'prompt-modal',
|
|
27
32
|
Snackbar: 'snackbar',
|
|
33
|
+
StartupCrash: 'startup-crash',
|
|
28
34
|
} as const;
|
|
29
35
|
|
|
30
36
|
export type UIComponent = ObjectValues<typeof UIComponents>;
|
|
31
37
|
|
|
38
|
+
export type ConfirmCheckboxes = Record<string, { label: string; default?: boolean; required?: boolean }>;
|
|
39
|
+
|
|
40
|
+
export interface ConfirmOptions {
|
|
41
|
+
acceptText?: string;
|
|
42
|
+
acceptColor?: Color;
|
|
43
|
+
cancelText?: string;
|
|
44
|
+
cancelColor?: Color;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ConfirmOptionsWithCheckboxes<T extends ConfirmCheckboxes = ConfirmCheckboxes> extends ConfirmOptions {
|
|
48
|
+
checkboxes?: T;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PromptOptions {
|
|
52
|
+
label?: string;
|
|
53
|
+
defaultValue?: string;
|
|
54
|
+
placeholder?: string;
|
|
55
|
+
acceptText?: string;
|
|
56
|
+
acceptColor?: Color;
|
|
57
|
+
cancelText?: string;
|
|
58
|
+
cancelColor?: Color;
|
|
59
|
+
trim?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
32
62
|
export interface ShowSnackbarOptions {
|
|
33
63
|
component?: Component;
|
|
34
64
|
color?: SnackbarColor;
|
|
@@ -47,43 +77,146 @@ export class UIService extends Service {
|
|
|
47
77
|
public alert(message: string): void;
|
|
48
78
|
public alert(title: string, message: string): void;
|
|
49
79
|
public alert(messageOrTitle: string, message?: string): void {
|
|
50
|
-
const
|
|
80
|
+
const getProperties = (): AGAlertModalProps => {
|
|
81
|
+
if (typeof message !== 'string') {
|
|
82
|
+
return { message: messageOrTitle };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
title: messageOrTitle,
|
|
87
|
+
message,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
51
90
|
|
|
52
|
-
this.openModal(this.requireComponent(UIComponents.AlertModal),
|
|
91
|
+
this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
|
|
53
92
|
}
|
|
54
93
|
|
|
55
|
-
|
|
56
|
-
public async confirm(
|
|
57
|
-
public async confirm(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
94
|
+
/* eslint-disable max-len */
|
|
95
|
+
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
96
|
+
public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
97
|
+
public async confirm<T extends ConfirmCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
98
|
+
public async confirm<T extends ConfirmCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
99
|
+
/* eslint-enable max-len */
|
|
100
|
+
|
|
101
|
+
public async confirm(
|
|
102
|
+
messageOrTitle: string,
|
|
103
|
+
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
104
|
+
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
105
|
+
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
106
|
+
const getProperties = (): AGConfirmModalProps => {
|
|
107
|
+
if (typeof messageOrOptions !== 'string') {
|
|
108
|
+
return {
|
|
109
|
+
message: messageOrTitle,
|
|
110
|
+
...(messageOrOptions ?? {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
title: messageOrTitle,
|
|
116
|
+
message: messageOrOptions,
|
|
117
|
+
...(options ?? {}),
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
const properties = getProperties();
|
|
121
|
+
const modal = await this.openModal<
|
|
122
|
+
ModalComponent<AGConfirmModalProps, boolean | [boolean, Record<string, boolean>]>
|
|
123
|
+
>(this.requireComponent(UIComponents.ConfirmModal), properties);
|
|
63
124
|
const result = await modal.beforeClose;
|
|
64
125
|
|
|
65
|
-
|
|
126
|
+
const confirmed = typeof result === 'object' ? result[0] : result ?? false;
|
|
127
|
+
const checkboxes =
|
|
128
|
+
typeof result === 'object'
|
|
129
|
+
? result[1]
|
|
130
|
+
: Object.entries(properties.checkboxes ?? {}).reduce(
|
|
131
|
+
(values, [checkbox, { default: defaultValue }]) => ({
|
|
132
|
+
[checkbox]: defaultValue ?? false,
|
|
133
|
+
...values,
|
|
134
|
+
}),
|
|
135
|
+
{} as Record<string, boolean>,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
|
|
139
|
+
if (!checkbox.required || checkboxes[name]) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (confirmed && App.development) {
|
|
144
|
+
// eslint-disable-next-line no-console
|
|
145
|
+
console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return [false, checkboxes];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
|
|
66
152
|
}
|
|
67
153
|
|
|
68
|
-
public async
|
|
69
|
-
public async
|
|
70
|
-
public async
|
|
71
|
-
|
|
154
|
+
public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
155
|
+
public async prompt(title: string, message: string, options?: PromptOptions): Promise<string | null>;
|
|
156
|
+
public async prompt(
|
|
157
|
+
messageOrTitle: string,
|
|
158
|
+
messageOrOptions?: string | PromptOptions,
|
|
159
|
+
options?: PromptOptions,
|
|
160
|
+
): Promise<string | null> {
|
|
161
|
+
const trim = options?.trim ?? true;
|
|
162
|
+
const getProperties = (): AGPromptModalProps => {
|
|
163
|
+
if (typeof messageOrOptions !== 'string') {
|
|
164
|
+
return {
|
|
165
|
+
message: messageOrTitle,
|
|
166
|
+
...(messageOrOptions ?? {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
72
169
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
170
|
+
return {
|
|
171
|
+
title: messageOrTitle,
|
|
172
|
+
message: messageOrOptions,
|
|
173
|
+
...(options ?? {}),
|
|
174
|
+
};
|
|
175
|
+
};
|
|
76
176
|
|
|
77
|
-
await this.
|
|
177
|
+
const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
|
|
178
|
+
this.requireComponent(UIComponents.PromptModal),
|
|
179
|
+
getProperties(),
|
|
180
|
+
);
|
|
181
|
+
const rawResult = await modal.beforeClose;
|
|
182
|
+
const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
|
|
78
183
|
|
|
79
|
-
return result;
|
|
184
|
+
return result ?? null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
|
|
188
|
+
public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
|
|
189
|
+
public async loading<T>(
|
|
190
|
+
messageOrOperation: string | Promise<T> | (() => T),
|
|
191
|
+
operation?: Promise<T> | (() => T),
|
|
192
|
+
): Promise<T> {
|
|
193
|
+
const getProperties = (): AGLoadingModalProps => {
|
|
194
|
+
if (typeof messageOrOperation !== 'string') {
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { message: messageOrOperation };
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), getProperties());
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
operation = typeof messageOrOperation === 'string' ? (operation as () => T) : messageOrOperation;
|
|
205
|
+
operation = typeof operation === 'function' ? Promise.resolve(operation()) : operation;
|
|
206
|
+
|
|
207
|
+
const [result] = await Promise.all([operation, after({ seconds: 1 })]);
|
|
208
|
+
|
|
209
|
+
return result;
|
|
210
|
+
} finally {
|
|
211
|
+
await this.closeModal(modal.id);
|
|
212
|
+
}
|
|
80
213
|
}
|
|
81
214
|
|
|
82
215
|
public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
|
|
83
216
|
const snackbar: Snackbar = {
|
|
84
217
|
id: uuid(),
|
|
85
218
|
properties: { message, ...options },
|
|
86
|
-
component: options.component ??
|
|
219
|
+
component: markRaw(options.component ?? this.requireComponent(UIComponents.Snackbar)),
|
|
87
220
|
};
|
|
88
221
|
|
|
89
222
|
this.setState('snackbars', this.snackbars.concat(snackbar));
|
|
@@ -137,9 +270,9 @@ export class UIService extends Service {
|
|
|
137
270
|
}
|
|
138
271
|
|
|
139
272
|
protected async boot(): Promise<void> {
|
|
140
|
-
await super.boot();
|
|
141
|
-
|
|
142
273
|
this.watchModalEvents();
|
|
274
|
+
this.watchMountedEvent();
|
|
275
|
+
this.watchViewportBreakpoints();
|
|
143
276
|
}
|
|
144
277
|
|
|
145
278
|
private watchModalEvents(): void {
|
|
@@ -167,16 +300,50 @@ export class UIService extends Service {
|
|
|
167
300
|
});
|
|
168
301
|
}
|
|
169
302
|
|
|
303
|
+
private watchMountedEvent(): void {
|
|
304
|
+
Events.once('application-mounted', async () => {
|
|
305
|
+
if (!globalThis.document || !globalThis.getComputedStyle) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const splash = globalThis.document.getElementById('splash');
|
|
310
|
+
|
|
311
|
+
if (!splash) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (globalThis.getComputedStyle(splash).opacity !== '0') {
|
|
316
|
+
splash.style.opacity = '0';
|
|
317
|
+
|
|
318
|
+
await after({ ms: 600 });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
splash.remove();
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private watchViewportBreakpoints(): void {
|
|
326
|
+
if (!globalThis.matchMedia) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
|
|
331
|
+
|
|
332
|
+
media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
|
|
333
|
+
}
|
|
334
|
+
|
|
170
335
|
}
|
|
171
336
|
|
|
172
|
-
export default facade(
|
|
337
|
+
export default facade(UIService);
|
|
173
338
|
|
|
174
339
|
declare module '@/services/Events' {
|
|
175
340
|
export interface EventsPayload {
|
|
176
|
-
'modal-will-close': { modal: Modal; result?: unknown };
|
|
177
|
-
'modal-closed': { modal: Modal; result?: unknown };
|
|
178
341
|
'close-modal': { id: string; result?: unknown };
|
|
179
342
|
'hide-modal': { id: string };
|
|
343
|
+
'hide-overlays-backdrop': void;
|
|
344
|
+
'modal-closed': { modal: Modal; result?: unknown };
|
|
345
|
+
'modal-will-close': { modal: Modal; result?: unknown };
|
|
180
346
|
'show-modal': { id: string };
|
|
347
|
+
'show-overlays-backdrop': void;
|
|
181
348
|
}
|
|
182
349
|
}
|
package/src/ui/index.ts
CHANGED
|
@@ -8,13 +8,17 @@ import AGAlertModal from '../components/modals/AGAlertModal.vue';
|
|
|
8
8
|
import AGConfirmModal from '../components/modals/AGConfirmModal.vue';
|
|
9
9
|
import AGErrorReportModal from '../components/modals/AGErrorReportModal.vue';
|
|
10
10
|
import AGLoadingModal from '../components/modals/AGLoadingModal.vue';
|
|
11
|
+
import AGPromptModal from '../components/modals/AGPromptModal.vue';
|
|
11
12
|
import AGSnackbar from '../components/snackbars/AGSnackbar.vue';
|
|
13
|
+
import AGStartupCrash from '../components/lib/AGStartupCrash.vue';
|
|
12
14
|
import type { UIComponent } from './UI';
|
|
13
15
|
|
|
14
|
-
export { UI, UIComponents, UIComponent };
|
|
15
|
-
|
|
16
16
|
const services = { $ui: UI };
|
|
17
17
|
|
|
18
|
+
export * from './UI';
|
|
19
|
+
export * from './utils';
|
|
20
|
+
export { default as UI } from './UI';
|
|
21
|
+
|
|
18
22
|
export type UIServices = typeof services;
|
|
19
23
|
|
|
20
24
|
export default definePlugin({
|
|
@@ -24,7 +28,9 @@ export default definePlugin({
|
|
|
24
28
|
[UIComponents.ConfirmModal]: AGConfirmModal,
|
|
25
29
|
[UIComponents.ErrorReportModal]: AGErrorReportModal,
|
|
26
30
|
[UIComponents.LoadingModal]: AGLoadingModal,
|
|
31
|
+
[UIComponents.PromptModal]: AGPromptModal,
|
|
27
32
|
[UIComponents.Snackbar]: AGSnackbar,
|
|
33
|
+
[UIComponents.StartupCrash]: AGStartupCrash,
|
|
28
34
|
};
|
|
29
35
|
|
|
30
36
|
Object.entries({
|
|
@@ -37,7 +43,7 @@ export default definePlugin({
|
|
|
37
43
|
});
|
|
38
44
|
|
|
39
45
|
declare module '@/bootstrap/options' {
|
|
40
|
-
interface AerogelOptions {
|
|
46
|
+
export interface AerogelOptions {
|
|
41
47
|
components?: Partial<Record<UIComponent, Component>>;
|
|
42
48
|
}
|
|
43
49
|
}
|
package/src/ui/utils.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const MOBILE_BREAKPOINT = 768;
|
|
2
|
+
|
|
3
|
+
export const Layouts = {
|
|
4
|
+
Mobile: 'mobile',
|
|
5
|
+
Desktop: 'desktop',
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
export type Layout = (typeof Layouts)[keyof typeof Layouts];
|
|
9
|
+
|
|
10
|
+
export function getCurrentLayout(): Layout {
|
|
11
|
+
if (globalThis.innerWidth > MOBILE_BREAKPOINT) {
|
|
12
|
+
return Layouts.Desktop;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return Layouts.Mobile;
|
|
16
|
+
}
|
|
@@ -14,6 +14,7 @@ export function useEvent<Event extends EventWithPayload>(
|
|
|
14
14
|
event: Event,
|
|
15
15
|
listener: EventListener<EventsPayload[Event]>
|
|
16
16
|
): void;
|
|
17
|
+
export function useEvent<Payload>(event: string, listener: (payload: Payload) => unknown): void;
|
|
17
18
|
export function useEvent<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): void;
|
|
18
19
|
|
|
19
20
|
export function useEvent(event: string, listener: EventListener): void {
|