@aerogel/core 0.0.0-next.b85327579d32f21c6a9fa21142f0165cdd320d7e → 0.0.0-next.f16bd1d894543c5303039c49f6f33488a1ffe931
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 +304 -47
- package/dist/aerogel-core.esm.js +1 -1
- package/dist/aerogel-core.esm.js.map +1 -1
- package/dist/virtual.d.ts +11 -0
- package/noeldemartin.config.js +4 -1
- package/package.json +2 -2
- package/src/components/AGAppModals.vue +15 -0
- package/src/components/AGAppOverlays.vue +5 -7
- package/src/components/AGAppSnackbars.vue +13 -0
- package/src/components/basic/AGMarkdown.vue +7 -2
- package/src/components/constants.ts +8 -0
- package/src/components/forms/AGButton.vue +25 -15
- package/src/components/headless/index.ts +1 -0
- package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +10 -0
- package/src/components/headless/snackbars/index.ts +25 -0
- package/src/components/index.ts +2 -0
- package/src/components/modals/AGConfirmModal.vue +1 -1
- package/src/components/modals/AGErrorReportModal.ts +20 -0
- package/src/components/modals/AGErrorReportModal.vue +62 -0
- package/src/components/modals/AGErrorReportModalButtons.vue +106 -0
- package/src/components/modals/AGErrorReportModalTitle.vue +25 -0
- package/src/components/modals/AGModal.vue +1 -1
- package/src/components/modals/index.ts +15 -2
- package/src/components/snackbars/AGSnackbar.vue +42 -0
- package/src/components/snackbars/index.ts +3 -0
- package/src/directives/index.ts +16 -3
- package/src/errors/Errors.ts +36 -7
- package/src/errors/index.ts +39 -1
- package/src/main.ts +0 -2
- package/src/services/App.state.ts +4 -1
- package/src/types/virtual.d.ts +11 -0
- package/src/ui/UI.state.ts +10 -1
- package/src/ui/UI.ts +37 -6
- package/src/ui/index.ts +4 -0
- package/src/utils/vue.ts +2 -0
- package/tsconfig.json +1 -0
- package/vite.config.ts +2 -1
- package/src/globals.ts +0 -6
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
:cancellable="cancellable"
|
|
6
6
|
class="relative z-50"
|
|
7
7
|
>
|
|
8
|
-
<div class="fixed inset-0 flex items-center justify-center">
|
|
8
|
+
<div class="fixed inset-0 flex items-center justify-center p-8">
|
|
9
9
|
<AGHeadlessModalPanel class="flex max-h-full max-w-full flex-col overflow-hidden bg-white">
|
|
10
10
|
<div class="flex max-h-full flex-col overflow-auto p-4">
|
|
11
11
|
<slot :close="close" />
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import AGAlertModal from './AGAlertModal.vue';
|
|
2
2
|
import AGConfirmModal from './AGConfirmModal.vue';
|
|
3
|
+
import AGErrorReportModalButtons from './AGErrorReportModalButtons.vue';
|
|
4
|
+
import AGErrorReportModalTitle from './AGErrorReportModalTitle.vue';
|
|
3
5
|
import AGLoadingModal from './AGLoadingModal.vue';
|
|
4
6
|
import AGModal from './AGModal.vue';
|
|
5
7
|
import AGModalContext from './AGModalContext.vue';
|
|
6
|
-
import { IAGModal } from './AGModal';
|
|
7
8
|
|
|
8
|
-
export
|
|
9
|
+
export * from './AGErrorReportModal';
|
|
10
|
+
export * from './AGModal';
|
|
11
|
+
export * from './AGModalContext';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
AGAlertModal,
|
|
15
|
+
AGConfirmModal,
|
|
16
|
+
AGErrorReportModalButtons,
|
|
17
|
+
AGErrorReportModalTitle,
|
|
18
|
+
AGLoadingModal,
|
|
19
|
+
AGModal,
|
|
20
|
+
AGModalContext,
|
|
21
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AGHeadlessSnackbar class="flex flex-row items-center justify-center gap-3 p-4" :class="styleClasses">
|
|
3
|
+
<AGMarkdown :text="message" raw />
|
|
4
|
+
<AGButton
|
|
5
|
+
v-for="(action, i) of actions"
|
|
6
|
+
:key="i"
|
|
7
|
+
:color="color"
|
|
8
|
+
@click="activate(action)"
|
|
9
|
+
>
|
|
10
|
+
{{ action.text }}
|
|
11
|
+
</AGButton>
|
|
12
|
+
</AGHeadlessSnackbar>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import { computed } from 'vue';
|
|
17
|
+
|
|
18
|
+
import UI from '@/ui/UI';
|
|
19
|
+
import { Colors } from '@/components/constants';
|
|
20
|
+
import { useSnackbarProps } from '@/components/headless';
|
|
21
|
+
import type { SnackbarAction } from '@/components/headless';
|
|
22
|
+
|
|
23
|
+
import AGButton from '../forms/AGButton.vue';
|
|
24
|
+
import AGHeadlessSnackbar from '../headless/snackbars/AGHeadlessSnackbar.vue';
|
|
25
|
+
import AGMarkdown from '../basic/AGMarkdown.vue';
|
|
26
|
+
|
|
27
|
+
const props = defineProps(useSnackbarProps());
|
|
28
|
+
const styleClasses = computed(() => {
|
|
29
|
+
switch (props.color) {
|
|
30
|
+
case Colors.Danger:
|
|
31
|
+
return 'bg-red-200 text-red-900';
|
|
32
|
+
default:
|
|
33
|
+
case Colors.Secondary:
|
|
34
|
+
return 'bg-gray-900 text-white';
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function activate(action: SnackbarAction): void {
|
|
39
|
+
action.handler?.();
|
|
40
|
+
action.dismiss && UI.hideSnackbar(props.id);
|
|
41
|
+
}
|
|
42
|
+
</script>
|
package/src/directives/index.ts
CHANGED
|
@@ -4,12 +4,25 @@ import { definePlugin } from '@/plugins';
|
|
|
4
4
|
|
|
5
5
|
import initialFocus from './initial-focus';
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const builtInDirectives: Record<string, Directive> = {
|
|
8
8
|
'initial-focus': initialFocus,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export default definePlugin({
|
|
12
|
-
install(app) {
|
|
13
|
-
|
|
12
|
+
install(app, options) {
|
|
13
|
+
const directives = {
|
|
14
|
+
...builtInDirectives,
|
|
15
|
+
...options.directives,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
for (const [name, directive] of Object.entries(directives)) {
|
|
19
|
+
app.directive(name, directive);
|
|
20
|
+
}
|
|
14
21
|
},
|
|
15
22
|
});
|
|
23
|
+
|
|
24
|
+
declare module '@/bootstrap/options' {
|
|
25
|
+
interface AerogelOptions {
|
|
26
|
+
directives?: Record<string, Directive>;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/errors/Errors.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { JSError, facade, isObject } from '@noeldemartin/utils';
|
|
1
|
+
import { JSError, facade, isObject, objectWithoutEmpty, toString } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
3
|
import App from '@/services/App';
|
|
4
4
|
import ServiceBootError from '@/errors/ServiceBootError';
|
|
5
|
-
import UI from '@/ui/UI';
|
|
6
|
-
import { translate } from '@/lang/utils';
|
|
5
|
+
import UI, { UIComponents } from '@/ui/UI';
|
|
6
|
+
import { translate, translateWithDefault } from '@/lang/utils';
|
|
7
7
|
|
|
8
8
|
import Service from './Errors.state';
|
|
9
|
+
import { Colors } from '@/components/constants';
|
|
9
10
|
import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
10
11
|
|
|
11
12
|
export class ErrorsService extends Service {
|
|
@@ -24,8 +25,13 @@ export class ErrorsService extends Service {
|
|
|
24
25
|
public async inspect(error: ErrorSource | ErrorReport[]): Promise<void> {
|
|
25
26
|
const reports = Array.isArray(error) ? error : [await this.createErrorReport(error)];
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
if (reports.length === 0) {
|
|
29
|
+
UI.alert(translateWithDefault('errors.inspectEmpty', 'Nothing to inspect!'));
|
|
30
|
+
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
UI.openModal(UI.requireComponent(UIComponents.ErrorReportModal), { reports });
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
public async report(error: ErrorSource, message?: string): Promise<void> {
|
|
@@ -54,8 +60,23 @@ export class ErrorsService extends Service {
|
|
|
54
60
|
date: new Date(),
|
|
55
61
|
};
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
UI.showSnackbar(
|
|
64
|
+
message ??
|
|
65
|
+
translateWithDefault('errors.notice', 'Something went wrong, but it\'s not your fault. Try again!'),
|
|
66
|
+
{
|
|
67
|
+
color: Colors.Danger,
|
|
68
|
+
actions: [
|
|
69
|
+
{
|
|
70
|
+
text: translateWithDefault('errors.viewDetails', 'View details'),
|
|
71
|
+
dismiss: true,
|
|
72
|
+
handler: () =>
|
|
73
|
+
UI.openModal(UI.requireComponent(UIComponents.ErrorReportModal), {
|
|
74
|
+
reports: [report],
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
);
|
|
59
80
|
|
|
60
81
|
this.setState({ logs: [log].concat(this.logs) });
|
|
61
82
|
}
|
|
@@ -102,6 +123,14 @@ export class ErrorsService extends Service {
|
|
|
102
123
|
return this.createErrorReportFromError(error);
|
|
103
124
|
}
|
|
104
125
|
|
|
126
|
+
if (isObject(error)) {
|
|
127
|
+
return objectWithoutEmpty({
|
|
128
|
+
title: toString(error['name'] ?? error['title'] ?? translate('errors.unknown')),
|
|
129
|
+
description: toString(error['message'] ?? error['description'] ?? ''),
|
|
130
|
+
error,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
105
134
|
return {
|
|
106
135
|
title: translate('errors.unknown'),
|
|
107
136
|
error,
|
package/src/errors/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { tap } from '@noeldemartin/utils';
|
|
2
|
+
|
|
1
3
|
import { bootServices } from '@/services';
|
|
2
4
|
import { definePlugin } from '@/plugins';
|
|
3
5
|
|
|
@@ -7,15 +9,51 @@ import { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
|
7
9
|
export { Errors, ErrorSource, ErrorReport, ErrorReportLog };
|
|
8
10
|
|
|
9
11
|
const services = { $errors: Errors };
|
|
12
|
+
const frameworkHandler: ErrorHandler = (error) => {
|
|
13
|
+
if (!Errors.instance) {
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
console.warn('Errors service hasn\'t been initialized properly!');
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line no-console
|
|
18
|
+
console.error(error);
|
|
19
|
+
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
10
22
|
|
|
23
|
+
Errors.report(error);
|
|
24
|
+
|
|
25
|
+
return true;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function setUpErrorHandler(baseHandler: ErrorHandler = () => false): ErrorHandler {
|
|
29
|
+
return tap(
|
|
30
|
+
(error) => baseHandler(error) || frameworkHandler(error),
|
|
31
|
+
(errorHandler) => {
|
|
32
|
+
globalThis.onerror = (message, _, __, ___, error) => errorHandler(error ?? message);
|
|
33
|
+
globalThis.onunhandledrejection = (event) => errorHandler(event.reason);
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ErrorHandler = (error: ErrorSource) => boolean;
|
|
11
39
|
export type ErrorsServices = typeof services;
|
|
12
40
|
|
|
13
41
|
export default definePlugin({
|
|
14
|
-
async install(app) {
|
|
42
|
+
async install(app, options) {
|
|
43
|
+
const errorHandler = setUpErrorHandler(options.handleError);
|
|
44
|
+
|
|
45
|
+
app.config.errorHandler = errorHandler;
|
|
46
|
+
|
|
15
47
|
await bootServices(app, services);
|
|
16
48
|
},
|
|
17
49
|
});
|
|
18
50
|
|
|
51
|
+
declare module '@/bootstrap/options' {
|
|
52
|
+
interface AerogelOptions {
|
|
53
|
+
handleError?(error: ErrorSource): boolean;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
19
57
|
declare module '@/services' {
|
|
20
58
|
export interface Services extends ErrorsServices {}
|
|
21
59
|
}
|
package/src/main.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import Build from 'virtual:aerogel';
|
|
2
|
+
|
|
1
3
|
import { defineServiceState } from '@/services/Service';
|
|
2
4
|
|
|
3
5
|
export default defineServiceState({
|
|
4
6
|
name: 'app',
|
|
5
7
|
initialState: {
|
|
6
|
-
environment:
|
|
8
|
+
environment: Build.environment,
|
|
9
|
+
sourceUrl: Build.sourceUrl,
|
|
7
10
|
isMounted: false,
|
|
8
11
|
},
|
|
9
12
|
computed: {
|
package/src/ui/UI.state.ts
CHANGED
|
@@ -17,7 +17,16 @@ export interface ModalComponent<
|
|
|
17
17
|
Result = unknown
|
|
18
18
|
> {}
|
|
19
19
|
|
|
20
|
+
export interface Snackbar {
|
|
21
|
+
id: string;
|
|
22
|
+
component: Component;
|
|
23
|
+
properties: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
export default defineServiceState({
|
|
21
27
|
name: 'ui',
|
|
22
|
-
initialState: {
|
|
28
|
+
initialState: {
|
|
29
|
+
modals: [] as Modal[],
|
|
30
|
+
snackbars: [] as Snackbar[],
|
|
31
|
+
},
|
|
23
32
|
});
|
package/src/ui/UI.ts
CHANGED
|
@@ -4,9 +4,10 @@ import type { Component } from 'vue';
|
|
|
4
4
|
import type { ObjectValues } from '@noeldemartin/utils';
|
|
5
5
|
|
|
6
6
|
import Events from '@/services/Events';
|
|
7
|
+
import type { SnackbarAction, SnackbarColor } from '@/components/headless/snackbars';
|
|
7
8
|
|
|
8
9
|
import Service from './UI.state';
|
|
9
|
-
import type { Modal, ModalComponent } from './UI.state';
|
|
10
|
+
import type { Modal, ModalComponent, Snackbar } from './UI.state';
|
|
10
11
|
|
|
11
12
|
interface ModalCallbacks<T = unknown> {
|
|
12
13
|
willClose(result: T | undefined): void;
|
|
@@ -21,16 +22,28 @@ type ModalResult<TComponent> = TComponent extends ModalComponent<Record<string,
|
|
|
21
22
|
export const UIComponents = {
|
|
22
23
|
AlertModal: 'alert-modal',
|
|
23
24
|
ConfirmModal: 'confirm-modal',
|
|
25
|
+
ErrorReportModal: 'error-report-modal',
|
|
24
26
|
LoadingModal: 'loading-modal',
|
|
27
|
+
Snackbar: 'snackbar',
|
|
25
28
|
} as const;
|
|
26
29
|
|
|
27
30
|
export type UIComponent = ObjectValues<typeof UIComponents>;
|
|
28
31
|
|
|
32
|
+
export interface ShowSnackbarOptions {
|
|
33
|
+
component?: Component;
|
|
34
|
+
color?: SnackbarColor;
|
|
35
|
+
actions?: SnackbarAction[];
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
export class UIService extends Service {
|
|
30
39
|
|
|
31
40
|
private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
|
|
32
41
|
private components: Partial<Record<UIComponent, Component>> = {};
|
|
33
42
|
|
|
43
|
+
public requireComponent(name: UIComponent): Component {
|
|
44
|
+
return this.components[name] ?? fail(`UI Component '${name}' is not defined!`);
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
public alert(message: string): void;
|
|
35
48
|
public alert(title: string, message: string): void;
|
|
36
49
|
public alert(messageOrTitle: string, message?: string): void {
|
|
@@ -66,6 +79,25 @@ export class UIService extends Service {
|
|
|
66
79
|
return result;
|
|
67
80
|
}
|
|
68
81
|
|
|
82
|
+
public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
|
|
83
|
+
const snackbar: Snackbar = {
|
|
84
|
+
id: uuid(),
|
|
85
|
+
properties: { message, ...options },
|
|
86
|
+
component: options.component ?? markRaw(this.requireComponent(UIComponents.Snackbar)),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
this.setState('snackbars', this.snackbars.concat(snackbar));
|
|
90
|
+
|
|
91
|
+
setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public hideSnackbar(id: string): void {
|
|
95
|
+
this.setState(
|
|
96
|
+
'snackbars',
|
|
97
|
+
this.snackbars.filter((snackbar) => snackbar.id !== id),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
69
101
|
public registerComponent(name: UIComponent, component: Component): void {
|
|
70
102
|
this.components[name] = component;
|
|
71
103
|
}
|
|
@@ -110,10 +142,6 @@ export class UIService extends Service {
|
|
|
110
142
|
this.watchModalEvents();
|
|
111
143
|
}
|
|
112
144
|
|
|
113
|
-
private requireComponent(name: UIComponent): Component {
|
|
114
|
-
return this.components[name] ?? fail(`UI Component '${name}' is not defined!`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
145
|
private watchModalEvents(): void {
|
|
118
146
|
Events.on('modal-will-close', ({ modal, result }) => {
|
|
119
147
|
this.modalCallbacks[modal.id]?.willClose?.(result);
|
|
@@ -124,7 +152,10 @@ export class UIService extends Service {
|
|
|
124
152
|
});
|
|
125
153
|
|
|
126
154
|
Events.on('modal-closed', async ({ modal, result }) => {
|
|
127
|
-
this.setState(
|
|
155
|
+
this.setState(
|
|
156
|
+
'modals',
|
|
157
|
+
this.modals.filter((m) => m.id !== modal.id),
|
|
158
|
+
);
|
|
128
159
|
|
|
129
160
|
this.modalCallbacks[modal.id]?.closed?.(result);
|
|
130
161
|
|
package/src/ui/index.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { definePlugin } from '@/plugins';
|
|
|
6
6
|
import UI, { UIComponents } from './UI';
|
|
7
7
|
import AGAlertModal from '../components/modals/AGAlertModal.vue';
|
|
8
8
|
import AGConfirmModal from '../components/modals/AGConfirmModal.vue';
|
|
9
|
+
import AGErrorReportModal from '../components/modals/AGErrorReportModal.vue';
|
|
9
10
|
import AGLoadingModal from '../components/modals/AGLoadingModal.vue';
|
|
11
|
+
import AGSnackbar from '../components/snackbars/AGSnackbar.vue';
|
|
10
12
|
import type { UIComponent } from './UI';
|
|
11
13
|
|
|
12
14
|
export { UI, UIComponents, UIComponent };
|
|
@@ -20,7 +22,9 @@ export default definePlugin({
|
|
|
20
22
|
const defaultComponents = {
|
|
21
23
|
[UIComponents.AlertModal]: AGAlertModal,
|
|
22
24
|
[UIComponents.ConfirmModal]: AGConfirmModal,
|
|
25
|
+
[UIComponents.ErrorReportModal]: AGErrorReportModal,
|
|
23
26
|
[UIComponents.LoadingModal]: AGLoadingModal,
|
|
27
|
+
[UIComponents.Snackbar]: AGSnackbar,
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
Object.entries({
|
package/src/utils/vue.ts
CHANGED
|
@@ -10,6 +10,8 @@ type BaseProp<T> = {
|
|
|
10
10
|
type RequiredProp<T> = BaseProp<T> & { required: true };
|
|
11
11
|
type OptionalProp<T> = BaseProp<T> & { default: T | (() => T) | null };
|
|
12
12
|
|
|
13
|
+
export type ComponentProps = Record<string, unknown>;
|
|
14
|
+
|
|
13
15
|
export function arrayProp<T>(defaultValue?: () => T[]): OptionalProp<T[]> {
|
|
14
16
|
return {
|
|
15
17
|
type: Array as PropType<T[]>,
|
package/tsconfig.json
CHANGED
package/vite.config.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import Aerogel from '@aerogel/vite';
|
|
2
|
+
import Icons from 'unplugin-icons/vite';
|
|
2
3
|
import { defineConfig } from 'vitest/config';
|
|
3
4
|
import { resolve } from 'path';
|
|
4
5
|
|
|
5
6
|
export default defineConfig({
|
|
6
7
|
test: { clearMocks: true },
|
|
7
|
-
plugins: [Aerogel()],
|
|
8
|
+
plugins: [Aerogel(), Icons()],
|
|
8
9
|
resolve: {
|
|
9
10
|
alias: {
|
|
10
11
|
'@': resolve(__dirname, './src'),
|