@aerogel/core 0.0.0-next.f16bd1d894543c5303039c49f6f33488a1ffe931 → 0.0.0-next.f1f5a990033d966dc0bb12d251110fbc9350dcc7
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 +702 -132
- 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 +4 -3
- package/src/bootstrap/index.ts +19 -3
- package/src/components/AGAppLayout.vue +7 -2
- package/src/components/AGAppOverlays.vue +5 -1
- package/src/components/AGAppSnackbars.vue +1 -1
- package/src/components/forms/AGSelect.story.vue +28 -0
- package/src/components/forms/AGSelect.vue +53 -0
- package/src/components/forms/index.ts +5 -6
- package/src/components/headless/forms/AGHeadlessButton.vue +4 -3
- package/src/components/headless/forms/AGHeadlessInput.ts +21 -1
- package/src/components/headless/forms/AGHeadlessInput.vue +8 -5
- package/src/components/headless/forms/AGHeadlessInputLabel.vue +8 -2
- package/src/components/headless/forms/AGHeadlessSelect.ts +39 -0
- package/src/components/headless/forms/AGHeadlessSelect.vue +76 -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/index.ts +9 -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 +1 -1
- package/src/components/lib/AGErrorMessage.vue +16 -0
- package/src/components/lib/AGLink.vue +9 -0
- package/src/components/{basic → lib}/AGMarkdown.vue +6 -10
- package/src/components/lib/AGMeasured.vue +15 -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 +27 -0
- package/src/components/modals/AGConfirmModal.vue +7 -11
- 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/index.ts +5 -0
- package/src/components/snackbars/AGSnackbar.vue +4 -10
- package/src/components/utils.ts +10 -0
- package/src/directives/index.ts +3 -1
- package/src/directives/measure.ts +12 -0
- package/src/errors/Errors.ts +37 -10
- package/src/errors/index.ts +9 -13
- package/src/forms/Form.ts +14 -6
- package/src/lang/Lang.ts +1 -1
- package/src/main.histoire.ts +1 -0
- package/src/plugins/Plugin.ts +1 -0
- package/src/plugins/index.ts +19 -0
- package/src/services/App.state.ts +7 -5
- package/src/services/App.ts +15 -3
- package/src/services/Service.ts +13 -4
- package/src/services/index.ts +7 -4
- package/src/ui/UI.ts +77 -16
- package/src/ui/index.ts +6 -3
- package/src/utils/composition/events.ts +1 -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 +13 -4
- package/tailwind.config.js +4 -0
- package/tsconfig.json +1 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<AGHeadlessSnackbar class="flex flex-row items-center justify-center gap-3 p-4" :class="styleClasses">
|
|
3
|
-
<AGMarkdown :text="message"
|
|
3
|
+
<AGMarkdown :text="message" inline />
|
|
4
4
|
<AGButton
|
|
5
5
|
v-for="(action, i) of actions"
|
|
6
6
|
:key="i"
|
|
@@ -15,16 +15,15 @@
|
|
|
15
15
|
<script setup lang="ts">
|
|
16
16
|
import { computed } from 'vue';
|
|
17
17
|
|
|
18
|
-
import UI from '@/ui/UI';
|
|
19
18
|
import { Colors } from '@/components/constants';
|
|
20
|
-
import { useSnackbarProps } from '@/components/headless';
|
|
21
|
-
import type { SnackbarAction } from '@/components/headless';
|
|
19
|
+
import { useSnackbar, useSnackbarProps } from '@/components/headless/snackbars';
|
|
22
20
|
|
|
23
21
|
import AGButton from '../forms/AGButton.vue';
|
|
24
22
|
import AGHeadlessSnackbar from '../headless/snackbars/AGHeadlessSnackbar.vue';
|
|
25
|
-
import AGMarkdown from '../
|
|
23
|
+
import AGMarkdown from '../lib/AGMarkdown.vue';
|
|
26
24
|
|
|
27
25
|
const props = defineProps(useSnackbarProps());
|
|
26
|
+
const { activate } = useSnackbar(props);
|
|
28
27
|
const styleClasses = computed(() => {
|
|
29
28
|
switch (props.color) {
|
|
30
29
|
case Colors.Danger:
|
|
@@ -34,9 +33,4 @@ const styleClasses = computed(() => {
|
|
|
34
33
|
return 'bg-gray-900 text-white';
|
|
35
34
|
}
|
|
36
35
|
});
|
|
37
|
-
|
|
38
|
-
function activate(action: SnackbarAction): void {
|
|
39
|
-
action.handler?.();
|
|
40
|
-
action.dismiss && UI.hideSnackbar(props.id);
|
|
41
|
-
}
|
|
42
36
|
</script>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function extractComponentProps<T extends Record<string, unknown>>(
|
|
2
|
+
values: Record<string, unknown>,
|
|
3
|
+
definitions: Record<string, unknown>,
|
|
4
|
+
): T {
|
|
5
|
+
return Object.keys(definitions).reduce((extracted, prop) => {
|
|
6
|
+
extracted[prop] = values[prop];
|
|
7
|
+
|
|
8
|
+
return extracted;
|
|
9
|
+
}, {} as Record<string, unknown>) as T;
|
|
10
|
+
}
|
package/src/directives/index.ts
CHANGED
|
@@ -3,9 +3,11 @@ import type { Directive } from 'vue';
|
|
|
3
3
|
import { definePlugin } from '@/plugins';
|
|
4
4
|
|
|
5
5
|
import initialFocus from './initial-focus';
|
|
6
|
+
import measure from './measure';
|
|
6
7
|
|
|
7
8
|
const builtInDirectives: Record<string, Directive> = {
|
|
8
9
|
'initial-focus': initialFocus,
|
|
10
|
+
'measure': measure,
|
|
9
11
|
};
|
|
10
12
|
|
|
11
13
|
export default definePlugin({
|
|
@@ -22,7 +24,7 @@ export default definePlugin({
|
|
|
22
24
|
});
|
|
23
25
|
|
|
24
26
|
declare module '@/bootstrap/options' {
|
|
25
|
-
interface AerogelOptions {
|
|
27
|
+
export interface AerogelOptions {
|
|
26
28
|
directives?: Record<string, Directive>;
|
|
27
29
|
}
|
|
28
30
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineDirective } from '@/utils/vue';
|
|
2
|
+
|
|
3
|
+
export default defineDirective({
|
|
4
|
+
mounted(element: HTMLElement, { value }: { value?: () => unknown }) {
|
|
5
|
+
const sizes = element.getBoundingClientRect();
|
|
6
|
+
|
|
7
|
+
element.style.setProperty('--width', `${sizes.width}px`);
|
|
8
|
+
element.style.setProperty('--height', `${sizes.height}px`);
|
|
9
|
+
|
|
10
|
+
value?.();
|
|
11
|
+
},
|
|
12
|
+
});
|
package/src/errors/Errors.ts
CHANGED
|
@@ -3,11 +3,13 @@ import { JSError, facade, isObject, objectWithoutEmpty, toString } from '@noelde
|
|
|
3
3
|
import App from '@/services/App';
|
|
4
4
|
import ServiceBootError from '@/errors/ServiceBootError';
|
|
5
5
|
import UI, { UIComponents } from '@/ui/UI';
|
|
6
|
-
import {
|
|
6
|
+
import { translateWithDefault } from '@/lang/utils';
|
|
7
7
|
|
|
8
8
|
import Service from './Errors.state';
|
|
9
9
|
import { Colors } from '@/components/constants';
|
|
10
|
+
import type { AGErrorReportModalProps } from '@/components/modals/AGErrorReportModal';
|
|
10
11
|
import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
12
|
+
import type { ModalComponent } from '@/ui/UI.state';
|
|
11
13
|
|
|
12
14
|
export class ErrorsService extends Service {
|
|
13
15
|
|
|
@@ -23,7 +25,7 @@ export class ErrorsService extends Service {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
public async inspect(error: ErrorSource | ErrorReport[]): Promise<void> {
|
|
26
|
-
const reports = Array.isArray(error) ? error : [await this.createErrorReport(error)];
|
|
28
|
+
const reports = Array.isArray(error) ? (error as ErrorReport[]) : [await this.createErrorReport(error)];
|
|
27
29
|
|
|
28
30
|
if (reports.length === 0) {
|
|
29
31
|
UI.alert(translateWithDefault('errors.inspectEmpty', 'Nothing to inspect!'));
|
|
@@ -31,11 +33,13 @@ export class ErrorsService extends Service {
|
|
|
31
33
|
return;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
UI.openModal(UI.requireComponent(UIComponents.ErrorReportModal), {
|
|
36
|
+
UI.openModal<ModalComponent<AGErrorReportModalProps>>(UI.requireComponent(UIComponents.ErrorReportModal), {
|
|
37
|
+
reports,
|
|
38
|
+
});
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
public async report(error: ErrorSource, message?: string): Promise<void> {
|
|
38
|
-
if (App.
|
|
42
|
+
if (App.development || App.testing) {
|
|
39
43
|
this.logError(error);
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -70,9 +74,10 @@ export class ErrorsService extends Service {
|
|
|
70
74
|
text: translateWithDefault('errors.viewDetails', 'View details'),
|
|
71
75
|
dismiss: true,
|
|
72
76
|
handler: () =>
|
|
73
|
-
UI.openModal(
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
UI.openModal<ModalComponent<AGErrorReportModalProps>>(
|
|
78
|
+
UI.requireComponent(UIComponents.ErrorReportModal),
|
|
79
|
+
{ reports: [report] },
|
|
80
|
+
),
|
|
76
81
|
},
|
|
77
82
|
],
|
|
78
83
|
},
|
|
@@ -105,6 +110,22 @@ export class ErrorsService extends Service {
|
|
|
105
110
|
});
|
|
106
111
|
}
|
|
107
112
|
|
|
113
|
+
public getErrorMessage(error: ErrorSource): string {
|
|
114
|
+
if (typeof error === 'string') {
|
|
115
|
+
return error;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (error instanceof Error || error instanceof JSError) {
|
|
119
|
+
return error.message;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isObject(error)) {
|
|
123
|
+
return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return translateWithDefault('errors.unknown', 'Unknown Error');
|
|
127
|
+
}
|
|
128
|
+
|
|
108
129
|
private logError(error: unknown): void {
|
|
109
130
|
// eslint-disable-next-line no-console
|
|
110
131
|
console.error(error);
|
|
@@ -125,14 +146,20 @@ export class ErrorsService extends Service {
|
|
|
125
146
|
|
|
126
147
|
if (isObject(error)) {
|
|
127
148
|
return objectWithoutEmpty({
|
|
128
|
-
title: toString(
|
|
129
|
-
|
|
149
|
+
title: toString(
|
|
150
|
+
error['name'] ?? error['title'] ?? translateWithDefault('errors.unknown', 'Unknown Error'),
|
|
151
|
+
),
|
|
152
|
+
description: toString(
|
|
153
|
+
error['message'] ??
|
|
154
|
+
error['description'] ??
|
|
155
|
+
translateWithDefault('errors.unknownDescription', 'Unknown error object'),
|
|
156
|
+
),
|
|
130
157
|
error,
|
|
131
158
|
});
|
|
132
159
|
}
|
|
133
160
|
|
|
134
161
|
return {
|
|
135
|
-
title:
|
|
162
|
+
title: translateWithDefault('errors.unknown', 'Unknown Error'),
|
|
136
163
|
error,
|
|
137
164
|
};
|
|
138
165
|
}
|
package/src/errors/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { App } from 'vue';
|
|
2
2
|
|
|
3
3
|
import { bootServices } from '@/services';
|
|
4
4
|
import { definePlugin } from '@/plugins';
|
|
@@ -25,14 +25,12 @@ const frameworkHandler: ErrorHandler = (error) => {
|
|
|
25
25
|
return true;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
function setUpErrorHandler(baseHandler: ErrorHandler = () => false):
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
);
|
|
28
|
+
function setUpErrorHandler(app: App, baseHandler: ErrorHandler = () => false): void {
|
|
29
|
+
const errorHandler: ErrorHandler = (error) => baseHandler(error) || frameworkHandler(error);
|
|
30
|
+
|
|
31
|
+
app.config.errorHandler = errorHandler;
|
|
32
|
+
globalThis.onerror = (event, _, __, ___, error) => errorHandler(error ?? event);
|
|
33
|
+
globalThis.onunhandledrejection = (event) => errorHandler(event.reason);
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
export type ErrorHandler = (error: ErrorSource) => boolean;
|
|
@@ -40,16 +38,14 @@ export type ErrorsServices = typeof services;
|
|
|
40
38
|
|
|
41
39
|
export default definePlugin({
|
|
42
40
|
async install(app, options) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
app.config.errorHandler = errorHandler;
|
|
41
|
+
setUpErrorHandler(app, options.handleError);
|
|
46
42
|
|
|
47
43
|
await bootServices(app, services);
|
|
48
44
|
},
|
|
49
45
|
});
|
|
50
46
|
|
|
51
47
|
declare module '@/bootstrap/options' {
|
|
52
|
-
interface AerogelOptions {
|
|
48
|
+
export interface AerogelOptions {
|
|
53
49
|
handleError?(error: ErrorSource): boolean;
|
|
54
50
|
}
|
|
55
51
|
}
|
package/src/forms/Form.ts
CHANGED
|
@@ -7,6 +7,7 @@ export const FormFieldTypes = {
|
|
|
7
7
|
String: 'string',
|
|
8
8
|
Number: 'number',
|
|
9
9
|
Boolean: 'boolean',
|
|
10
|
+
Object: 'object',
|
|
10
11
|
} as const;
|
|
11
12
|
|
|
12
13
|
export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
|
|
@@ -36,15 +37,18 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
|
|
|
36
37
|
? number
|
|
37
38
|
: TType extends typeof FormFieldTypes.Boolean
|
|
38
39
|
? boolean
|
|
40
|
+
: TType extends typeof FormFieldTypes.Object
|
|
41
|
+
? object
|
|
39
42
|
: never;
|
|
40
43
|
|
|
44
|
+
const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
|
|
45
|
+
|
|
41
46
|
export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
|
|
42
47
|
|
|
43
48
|
public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
|
|
44
49
|
|
|
45
50
|
private _fields: Fields;
|
|
46
51
|
private _data: FormData<Fields>;
|
|
47
|
-
private _valid: ComputedRef<boolean>;
|
|
48
52
|
private _submitted: Ref<boolean>;
|
|
49
53
|
private _errors: FormErrors<Fields>;
|
|
50
54
|
|
|
@@ -55,13 +59,17 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
55
59
|
this._submitted = ref(false);
|
|
56
60
|
this._data = this.getInitialData(fields);
|
|
57
61
|
this._errors = this.getInitialErrors(fields);
|
|
58
|
-
|
|
62
|
+
|
|
63
|
+
validForms.set(
|
|
64
|
+
this,
|
|
65
|
+
computed(() => !Object.values(this._errors).some((error) => error !== null)),
|
|
66
|
+
);
|
|
59
67
|
|
|
60
68
|
this.errors = readonly(this._errors);
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
public get valid(): boolean {
|
|
64
|
-
return this
|
|
72
|
+
return !!validForms.get(this)?.value;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
public get submitted(): boolean {
|
|
@@ -92,11 +100,11 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
92
100
|
return this.valid;
|
|
93
101
|
}
|
|
94
102
|
|
|
95
|
-
public reset(): void {
|
|
103
|
+
public reset(options: { keepData?: boolean; keepErrors?: boolean } = {}): void {
|
|
96
104
|
this._submitted.value = false;
|
|
97
105
|
|
|
98
|
-
this.resetData();
|
|
99
|
-
this.resetErrors();
|
|
106
|
+
options.keepData || this.resetData();
|
|
107
|
+
options.keepErrors || this.resetErrors();
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
public submit(): boolean {
|
package/src/lang/Lang.ts
CHANGED
|
@@ -17,7 +17,7 @@ export class LangService extends Service {
|
|
|
17
17
|
this.provider = {
|
|
18
18
|
translate: (key) => {
|
|
19
19
|
// eslint-disable-next-line no-console
|
|
20
|
-
App.
|
|
20
|
+
App.development && console.warn('Lang provider is missing');
|
|
21
21
|
|
|
22
22
|
return key;
|
|
23
23
|
},
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './assets/histoire.css';
|
package/src/plugins/Plugin.ts
CHANGED
package/src/plugins/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { GetClosureArgs } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
import App from '@/services/App';
|
|
4
|
+
|
|
1
5
|
import type { Plugin } from './Plugin';
|
|
2
6
|
|
|
3
7
|
export * from './Plugin';
|
|
@@ -5,3 +9,18 @@ export * from './Plugin';
|
|
|
5
9
|
export function definePlugin<T extends Plugin>(plugin: T): T {
|
|
6
10
|
return plugin;
|
|
7
11
|
}
|
|
12
|
+
|
|
13
|
+
export async function installPlugins(plugins: Plugin[], ...args: GetClosureArgs<Plugin['install']>): Promise<void> {
|
|
14
|
+
App.setState(
|
|
15
|
+
'plugins',
|
|
16
|
+
plugins.reduce((pluginsMap, plugin) => {
|
|
17
|
+
if (plugin.name) {
|
|
18
|
+
pluginsMap[plugin.name] = plugin;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return pluginsMap;
|
|
22
|
+
}, {} as Record<string, Plugin>),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
await Promise.all(plugins.map((plugin) => plugin.install(...args)) ?? []);
|
|
26
|
+
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Aerogel from 'virtual:aerogel';
|
|
2
2
|
|
|
3
3
|
import { defineServiceState } from '@/services/Service';
|
|
4
|
+
import type { Plugin } from '@/plugins/Plugin';
|
|
4
5
|
|
|
5
6
|
export default defineServiceState({
|
|
6
7
|
name: 'app',
|
|
7
8
|
initialState: {
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
plugins: {} as Record<string, Plugin>,
|
|
10
|
+
environment: Aerogel.environment,
|
|
11
|
+
sourceUrl: Aerogel.sourceUrl,
|
|
10
12
|
isMounted: false,
|
|
11
13
|
},
|
|
12
14
|
computed: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
development: (state) => state.environment === 'development',
|
|
16
|
+
testing: (state) => state.environment === 'testing',
|
|
15
17
|
},
|
|
16
18
|
});
|
package/src/services/App.ts
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
import { facade } from '@noeldemartin/utils';
|
|
1
|
+
import { facade, forever, updateLocationQueryParameters } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
3
|
import Events from '@/services/Events';
|
|
4
|
+
import type { Plugin } from '@/plugins';
|
|
4
5
|
|
|
5
6
|
import Service from './App.state';
|
|
6
7
|
|
|
7
8
|
export class AppService extends Service {
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
public async reload(queryParameters?: Record<string, string | undefined>): Promise<void> {
|
|
11
|
+
queryParameters && updateLocationQueryParameters(queryParameters);
|
|
12
|
+
|
|
13
|
+
location.reload();
|
|
14
|
+
|
|
15
|
+
// Stall until the reload happens
|
|
16
|
+
await forever();
|
|
17
|
+
}
|
|
11
18
|
|
|
19
|
+
public plugin<T extends Plugin = Plugin>(name: string): T | null {
|
|
20
|
+
return (this.plugins[name] as T) ?? null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
protected async boot(): Promise<void> {
|
|
12
24
|
Events.once('application-mounted', () => this.setState({ isMounted: true }));
|
|
13
25
|
}
|
|
14
26
|
|
package/src/services/Service.ts
CHANGED
|
@@ -93,7 +93,8 @@ export default class Service<
|
|
|
93
93
|
const handleError = (error: unknown) => this._booted.reject(new ServiceBootError(this._name, error));
|
|
94
94
|
|
|
95
95
|
try {
|
|
96
|
-
this.
|
|
96
|
+
this.frameworkBoot()
|
|
97
|
+
.then(() => this.boot())
|
|
97
98
|
.then(() => this._booted.resolve())
|
|
98
99
|
.catch(handleError);
|
|
99
100
|
} catch (error) {
|
|
@@ -161,7 +162,11 @@ export default class Service<
|
|
|
161
162
|
return;
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
const storage = Storage.
|
|
165
|
+
const storage = Storage.get<ServiceStorage>(this._name);
|
|
166
|
+
|
|
167
|
+
if (!storage) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
165
170
|
|
|
166
171
|
Storage.set(this._name, {
|
|
167
172
|
...storage,
|
|
@@ -189,11 +194,15 @@ export default class Service<
|
|
|
189
194
|
return state;
|
|
190
195
|
}
|
|
191
196
|
|
|
197
|
+
protected async frameworkBoot(): Promise<void> {
|
|
198
|
+
this.initializePersistedState();
|
|
199
|
+
}
|
|
200
|
+
|
|
192
201
|
protected async boot(): Promise<void> {
|
|
193
|
-
|
|
202
|
+
// Placeholder for overrides, don't place any functionality here.
|
|
194
203
|
}
|
|
195
204
|
|
|
196
|
-
protected
|
|
205
|
+
protected initializePersistedState(): void {
|
|
197
206
|
// TODO fix this.static()
|
|
198
207
|
const persist = (this.constructor as unknown as { persist: string[] }).persist;
|
|
199
208
|
|
package/src/services/index.ts
CHANGED
|
@@ -24,13 +24,16 @@ export interface Services extends DefaultServices {}
|
|
|
24
24
|
|
|
25
25
|
export async function bootServices(app: VueApp, services: Record<string, Service>): Promise<void> {
|
|
26
26
|
await Promise.all(
|
|
27
|
-
Object.entries(services).map(async ([
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
Object.entries(services).map(async ([name, service]) => {
|
|
28
|
+
await service
|
|
29
|
+
.launch()
|
|
30
|
+
.catch((error) => app.config.errorHandler?.(error, null, `Failed launching ${name}.`));
|
|
30
31
|
}),
|
|
31
32
|
);
|
|
32
33
|
|
|
33
34
|
Object.assign(app.config.globalProperties, services);
|
|
35
|
+
|
|
36
|
+
App.development && Object.assign(window, services);
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
export default definePlugin({
|
|
@@ -47,7 +50,7 @@ export default definePlugin({
|
|
|
47
50
|
});
|
|
48
51
|
|
|
49
52
|
declare module '@/bootstrap/options' {
|
|
50
|
-
interface AerogelOptions {
|
|
53
|
+
export interface AerogelOptions {
|
|
51
54
|
services?: Record<string, Service>;
|
|
52
55
|
}
|
|
53
56
|
}
|
package/src/ui/UI.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
@@ -8,6 +8,7 @@ import type { SnackbarAction, SnackbarColor } from '@/components/headless/snackb
|
|
|
8
8
|
|
|
9
9
|
import Service from './UI.state';
|
|
10
10
|
import type { Modal, ModalComponent, Snackbar } from './UI.state';
|
|
11
|
+
import type { AGAlertModalProps, AGConfirmModalProps, AGLoadingModalProps } from '@/components';
|
|
11
12
|
|
|
12
13
|
interface ModalCallbacks<T = unknown> {
|
|
13
14
|
willClose(result: T | undefined): void;
|
|
@@ -25,10 +26,16 @@ export const UIComponents = {
|
|
|
25
26
|
ErrorReportModal: 'error-report-modal',
|
|
26
27
|
LoadingModal: 'loading-modal',
|
|
27
28
|
Snackbar: 'snackbar',
|
|
29
|
+
StartupCrash: 'startup-crash',
|
|
28
30
|
} as const;
|
|
29
31
|
|
|
30
32
|
export type UIComponent = ObjectValues<typeof UIComponents>;
|
|
31
33
|
|
|
34
|
+
export interface ConfirmOptions {
|
|
35
|
+
acceptText?: string;
|
|
36
|
+
cancelText?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
export interface ShowSnackbarOptions {
|
|
33
40
|
component?: Component;
|
|
34
41
|
color?: SnackbarColor;
|
|
@@ -47,18 +54,45 @@ export class UIService extends Service {
|
|
|
47
54
|
public alert(message: string): void;
|
|
48
55
|
public alert(title: string, message: string): void;
|
|
49
56
|
public alert(messageOrTitle: string, message?: string): void {
|
|
50
|
-
const
|
|
57
|
+
const getProperties = (): AGAlertModalProps => {
|
|
58
|
+
if (typeof message !== 'string') {
|
|
59
|
+
return { message: messageOrTitle };
|
|
60
|
+
}
|
|
51
61
|
|
|
52
|
-
|
|
62
|
+
return {
|
|
63
|
+
title: messageOrTitle,
|
|
64
|
+
message,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
|
|
53
69
|
}
|
|
54
70
|
|
|
55
|
-
public async confirm(message: string): Promise<boolean>;
|
|
56
|
-
public async confirm(title: string, message: string): Promise<boolean>;
|
|
57
|
-
public async confirm(
|
|
58
|
-
|
|
71
|
+
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
72
|
+
public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
73
|
+
public async confirm(
|
|
74
|
+
messageOrTitle: string,
|
|
75
|
+
messageOrOptions?: string | ConfirmOptions,
|
|
76
|
+
options?: ConfirmOptions,
|
|
77
|
+
): Promise<boolean> {
|
|
78
|
+
const getProperties = (): AGConfirmModalProps => {
|
|
79
|
+
if (typeof messageOrOptions !== 'string') {
|
|
80
|
+
return {
|
|
81
|
+
message: messageOrTitle,
|
|
82
|
+
...(messageOrOptions ?? {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
title: messageOrTitle,
|
|
88
|
+
message: messageOrOptions,
|
|
89
|
+
...(options ?? {}),
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
59
93
|
const modal = await this.openModal<ModalComponent<{ message: string }, boolean>>(
|
|
60
94
|
this.requireComponent(UIComponents.ConfirmModal),
|
|
61
|
-
|
|
95
|
+
getProperties(),
|
|
62
96
|
);
|
|
63
97
|
const result = await modal.beforeClose;
|
|
64
98
|
|
|
@@ -68,15 +102,25 @@ export class UIService extends Service {
|
|
|
68
102
|
public async loading<T>(operation: Promise<T>): Promise<T>;
|
|
69
103
|
public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
|
|
70
104
|
public async loading<T>(messageOrOperation: string | Promise<T>, operation?: Promise<T>): Promise<T> {
|
|
71
|
-
|
|
105
|
+
const getProperties = (): AGLoadingModalProps => {
|
|
106
|
+
if (typeof messageOrOperation !== 'string') {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { message: messageOrOperation };
|
|
111
|
+
};
|
|
72
112
|
|
|
73
|
-
const
|
|
74
|
-
const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), { message });
|
|
75
|
-
const result = await operation;
|
|
113
|
+
const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), getProperties());
|
|
76
114
|
|
|
77
|
-
|
|
115
|
+
try {
|
|
116
|
+
operation = typeof messageOrOperation === 'string' ? (operation as Promise<T>) : messageOrOperation;
|
|
78
117
|
|
|
79
|
-
|
|
118
|
+
const [result] = await Promise.all([operation, after({ seconds: 1 })]);
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
} finally {
|
|
122
|
+
await this.closeModal(modal.id);
|
|
123
|
+
}
|
|
80
124
|
}
|
|
81
125
|
|
|
82
126
|
public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
|
|
@@ -137,9 +181,8 @@ export class UIService extends Service {
|
|
|
137
181
|
}
|
|
138
182
|
|
|
139
183
|
protected async boot(): Promise<void> {
|
|
140
|
-
await super.boot();
|
|
141
|
-
|
|
142
184
|
this.watchModalEvents();
|
|
185
|
+
this.watchMountedEvent();
|
|
143
186
|
}
|
|
144
187
|
|
|
145
188
|
private watchModalEvents(): void {
|
|
@@ -167,6 +210,24 @@ export class UIService extends Service {
|
|
|
167
210
|
});
|
|
168
211
|
}
|
|
169
212
|
|
|
213
|
+
private watchMountedEvent(): void {
|
|
214
|
+
Events.once('application-mounted', async () => {
|
|
215
|
+
const splash = document.getElementById('splash');
|
|
216
|
+
|
|
217
|
+
if (!splash) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (window.getComputedStyle(splash).opacity !== '0') {
|
|
222
|
+
splash.style.opacity = '0';
|
|
223
|
+
|
|
224
|
+
await after({ ms: 600 });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
splash.remove();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
170
231
|
}
|
|
171
232
|
|
|
172
233
|
export default facade(new UIService());
|
package/src/ui/index.ts
CHANGED
|
@@ -9,12 +9,14 @@ 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
11
|
import AGSnackbar from '../components/snackbars/AGSnackbar.vue';
|
|
12
|
+
import AGStartupCrash from '../components/lib/AGStartupCrash.vue';
|
|
12
13
|
import type { UIComponent } from './UI';
|
|
13
14
|
|
|
14
|
-
export { UI, UIComponents, UIComponent };
|
|
15
|
-
|
|
16
15
|
const services = { $ui: UI };
|
|
17
16
|
|
|
17
|
+
export * from './UI';
|
|
18
|
+
export { default as UI } from './UI';
|
|
19
|
+
|
|
18
20
|
export type UIServices = typeof services;
|
|
19
21
|
|
|
20
22
|
export default definePlugin({
|
|
@@ -25,6 +27,7 @@ export default definePlugin({
|
|
|
25
27
|
[UIComponents.ErrorReportModal]: AGErrorReportModal,
|
|
26
28
|
[UIComponents.LoadingModal]: AGLoadingModal,
|
|
27
29
|
[UIComponents.Snackbar]: AGSnackbar,
|
|
30
|
+
[UIComponents.StartupCrash]: AGStartupCrash,
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
Object.entries({
|
|
@@ -37,7 +40,7 @@ export default definePlugin({
|
|
|
37
40
|
});
|
|
38
41
|
|
|
39
42
|
declare module '@/bootstrap/options' {
|
|
40
|
-
interface AerogelOptions {
|
|
43
|
+
export interface AerogelOptions {
|
|
41
44
|
components?: Partial<Record<UIComponent, Component>>;
|
|
42
45
|
}
|
|
43
46
|
}
|
|
@@ -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 {
|