@aerogel/core 0.0.0-next.7f6ed5a1f91688a86bf5ede2adc465e4fd6cfdea → 0.0.0-next.8323c60b905020dcb3bd9d4b0bc8d9b6529e1082
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.css +1 -0
- package/dist/aerogel-core.d.ts +2547 -422
- package/dist/aerogel-core.js +3722 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +39 -37
- package/src/bootstrap/bootstrap.test.ts +7 -66
- package/src/bootstrap/index.ts +46 -33
- package/src/bootstrap/options.ts +8 -1
- package/src/components/AppLayout.vue +14 -0
- package/src/components/AppModals.vue +14 -0
- package/src/components/AppOverlays.vue +9 -0
- package/src/components/AppToasts.vue +16 -0
- package/src/components/contracts/AlertModal.ts +19 -0
- package/src/components/contracts/Button.ts +16 -0
- package/src/components/contracts/ConfirmModal.ts +48 -0
- package/src/components/contracts/DropdownMenu.ts +25 -0
- package/src/components/contracts/ErrorReportModal.ts +33 -0
- package/src/components/contracts/Input.ts +26 -0
- package/src/components/contracts/LoadingModal.ts +26 -0
- package/src/components/contracts/Modal.ts +21 -0
- package/src/components/contracts/PromptModal.ts +34 -0
- package/src/components/contracts/Select.ts +45 -0
- package/src/components/contracts/Toast.ts +15 -0
- package/src/components/contracts/index.ts +11 -0
- package/src/components/headless/HeadlessButton.vue +51 -0
- package/src/components/headless/HeadlessInput.vue +59 -0
- package/src/components/headless/HeadlessInputDescription.vue +27 -0
- package/src/components/headless/HeadlessInputError.vue +22 -0
- package/src/components/headless/HeadlessInputInput.vue +86 -0
- package/src/components/headless/HeadlessInputLabel.vue +18 -0
- package/src/components/headless/HeadlessInputTextArea.vue +40 -0
- package/src/components/headless/HeadlessModal.vue +57 -0
- package/src/components/headless/HeadlessModalContent.vue +30 -0
- package/src/components/headless/HeadlessModalDescription.vue +12 -0
- package/src/components/headless/HeadlessModalOverlay.vue +12 -0
- package/src/components/headless/HeadlessModalTitle.vue +12 -0
- package/src/components/headless/HeadlessSelect.vue +120 -0
- package/src/components/headless/HeadlessSelectError.vue +25 -0
- package/src/components/headless/HeadlessSelectLabel.vue +25 -0
- package/src/components/headless/HeadlessSelectOption.vue +34 -0
- package/src/components/headless/HeadlessSelectOptions.vue +42 -0
- package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
- package/src/components/headless/HeadlessSelectValue.vue +18 -0
- package/src/components/headless/HeadlessSwitch.vue +96 -0
- package/src/components/headless/HeadlessToast.vue +18 -0
- package/src/components/headless/HeadlessToastAction.vue +13 -0
- package/src/components/headless/index.ts +20 -2
- package/src/components/index.ts +6 -6
- package/src/components/ui/AdvancedOptions.vue +18 -0
- package/src/components/ui/AlertModal.vue +17 -0
- package/src/components/ui/Button.vue +115 -0
- package/src/components/ui/Checkbox.vue +56 -0
- package/src/components/ui/ConfirmModal.vue +50 -0
- package/src/components/ui/DropdownMenu.vue +32 -0
- package/src/components/ui/DropdownMenuOption.vue +22 -0
- package/src/components/ui/DropdownMenuOptions.vue +44 -0
- package/src/components/ui/EditableContent.vue +82 -0
- package/src/components/ui/ErrorLogs.vue +19 -0
- package/src/components/ui/ErrorLogsModal.vue +48 -0
- package/src/components/ui/ErrorMessage.vue +15 -0
- package/src/components/ui/ErrorReportModal.vue +73 -0
- package/src/components/ui/ErrorReportModalButtons.vue +118 -0
- package/src/components/ui/ErrorReportModalTitle.vue +24 -0
- package/src/components/ui/Form.vue +24 -0
- package/src/components/ui/Input.vue +56 -0
- package/src/components/ui/Link.vue +12 -0
- package/src/components/ui/LoadingModal.vue +34 -0
- package/src/components/ui/Markdown.vue +97 -0
- package/src/components/ui/Modal.vue +131 -0
- package/src/components/ui/ModalContext.vue +31 -0
- package/src/components/ui/ProgressBar.vue +51 -0
- package/src/components/ui/PromptModal.vue +38 -0
- package/src/components/ui/Select.vue +27 -0
- package/src/components/ui/SelectLabel.vue +21 -0
- package/src/components/ui/SelectOption.vue +29 -0
- package/src/components/ui/SelectOptions.vue +35 -0
- package/src/components/ui/SelectTrigger.vue +29 -0
- package/src/components/ui/Setting.vue +31 -0
- package/src/components/ui/SettingsModal.vue +15 -0
- package/src/components/ui/StartupCrash.vue +31 -0
- package/src/components/ui/Switch.vue +11 -0
- package/src/components/ui/TextArea.vue +56 -0
- package/src/components/ui/Toast.vue +46 -0
- package/src/components/ui/index.ts +35 -0
- package/src/directives/index.ts +29 -6
- package/src/directives/measure.ts +46 -0
- package/src/errors/Errors.state.ts +31 -0
- package/src/errors/Errors.ts +200 -0
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +53 -0
- package/src/errors/settings/Debug.vue +32 -0
- package/src/errors/settings/index.ts +10 -0
- package/src/errors/utils.ts +35 -0
- package/src/forms/FormController.test.ts +113 -0
- package/src/forms/FormController.ts +255 -0
- package/src/forms/index.ts +3 -2
- package/src/forms/utils.ts +87 -14
- package/src/forms/validation.ts +50 -0
- package/src/index.css +76 -0
- package/src/{main.ts → index.ts} +5 -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 +63 -9
- package/src/lang/index.ts +22 -75
- package/src/lang/settings/Language.vue +48 -0
- package/src/lang/settings/index.ts +10 -0
- package/src/lang/utils.ts +4 -0
- package/src/plugins/Plugin.ts +8 -0
- package/src/plugins/index.ts +29 -0
- package/src/services/App.state.ts +50 -0
- package/src/services/App.ts +63 -0
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.test.ts +39 -0
- package/src/services/Events.ts +110 -36
- package/src/services/Service.ts +273 -35
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +45 -8
- package/src/services/store.ts +30 -0
- package/src/services/utils.ts +18 -0
- package/src/testing/index.ts +30 -0
- package/src/testing/setup.ts +11 -0
- package/src/types/vite.d.ts +0 -2
- package/src/ui/UI.state.ts +21 -13
- package/src/ui/UI.ts +350 -53
- package/src/ui/index.ts +40 -25
- package/src/ui/utils.ts +16 -0
- package/src/utils/app.ts +7 -0
- package/src/utils/classes.ts +41 -0
- package/src/utils/composition/events.ts +4 -5
- package/src/utils/composition/forms.ts +27 -0
- package/src/utils/composition/hooks.ts +9 -0
- 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 +33 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/markdown.test.ts +50 -0
- package/src/utils/markdown.ts +60 -4
- package/src/utils/types.ts +3 -0
- package/src/utils/vue.ts +38 -121
- package/.eslintrc.js +0 -3
- 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/noeldemartin.config.js +0 -2
- package/src/bootstrap/hooks.ts +0 -19
- package/src/components/AGAppLayout.vue +0 -11
- package/src/components/AGAppOverlays.vue +0 -39
- package/src/components/basic/AGMarkdown.vue +0 -20
- package/src/components/basic/index.ts +0 -3
- package/src/components/forms/AGButton.vue +0 -11
- package/src/components/forms/AGForm.vue +0 -26
- package/src/components/forms/AGInput.vue +0 -32
- package/src/components/forms/index.ts +0 -5
- package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
- package/src/components/headless/forms/AGHeadlessInput.ts +0 -8
- package/src/components/headless/forms/AGHeadlessInput.vue +0 -54
- package/src/components/headless/forms/AGHeadlessInputError.vue +0 -22
- package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -29
- package/src/components/headless/forms/index.ts +0 -4
- package/src/components/headless/modals/AGHeadlessModal.ts +0 -7
- package/src/components/headless/modals/AGHeadlessModal.vue +0 -84
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -20
- package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
- package/src/components/headless/modals/index.ts +0 -6
- package/src/components/modals/AGAlertModal.vue +0 -15
- package/src/components/modals/AGModal.ts +0 -6
- package/src/components/modals/AGModal.vue +0 -18
- package/src/components/modals/AGModalContext.ts +0 -8
- package/src/components/modals/AGModalContext.vue +0 -22
- package/src/components/modals/index.ts +0 -5
- package/src/directives/initial-focus.ts +0 -11
- package/src/forms/Form.test.ts +0 -37
- package/src/forms/Form.ts +0 -154
- package/src/forms/composition.ts +0 -6
- package/src/lang/helpers.ts +0 -5
- package/src/models/index.ts +0 -18
- package/src/routing/index.ts +0 -33
- package/src/testing/stubs/lang/en.yaml +0 -1
- package/src/testing/stubs/models/User.ts +0 -3
- package/tsconfig.json +0 -19
- package/vite.config.ts +0 -17
package/src/directives/index.ts
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
import type { Directive } from 'vue';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import measure from './measure';
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const builtInDirectives: Record<string, Directive> = {
|
|
8
|
+
measure: measure,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
export
|
|
12
|
-
|
|
11
|
+
export * from './measure';
|
|
12
|
+
|
|
13
|
+
export default definePlugin({
|
|
14
|
+
install(app, options) {
|
|
15
|
+
const directives = {
|
|
16
|
+
...builtInDirectives,
|
|
17
|
+
...options.directives,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
for (const [name, directive] of Object.entries(directives)) {
|
|
21
|
+
app.directive(name, directive);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
13
24
|
});
|
|
25
|
+
|
|
26
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
27
|
+
export interface AerogelOptions {
|
|
28
|
+
directives?: Record<string, Directive>;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
declare module 'vue' {
|
|
33
|
+
interface ComponentCustomDirectives {
|
|
34
|
+
measure: Directive<string, string>;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineDirective } from '@aerogel/core/utils/vue';
|
|
2
|
+
import { tap } from '@noeldemartin/utils';
|
|
3
|
+
|
|
4
|
+
const resizeObservers: WeakMap<HTMLElement, ResizeObserver> = new WeakMap();
|
|
5
|
+
|
|
6
|
+
export type MeasureDirectiveValue =
|
|
7
|
+
| MeasureDirectiveListener
|
|
8
|
+
| {
|
|
9
|
+
css?: boolean;
|
|
10
|
+
watch?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type MeasureDirectiveModifiers = 'css' | 'watch';
|
|
14
|
+
|
|
15
|
+
export interface ElementSize {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type MeasureDirectiveListener = (size: ElementSize) => unknown;
|
|
21
|
+
|
|
22
|
+
export default defineDirective<MeasureDirectiveValue, MeasureDirectiveModifiers>({
|
|
23
|
+
mounted(element: HTMLElement, { value, modifiers }) {
|
|
24
|
+
const listener = typeof value === 'function' ? (value as MeasureDirectiveListener) : null;
|
|
25
|
+
const update = () => {
|
|
26
|
+
const sizes = element.getBoundingClientRect();
|
|
27
|
+
|
|
28
|
+
if (modifiers.css) {
|
|
29
|
+
element.style.setProperty('--width', `${sizes.width}px`);
|
|
30
|
+
element.style.setProperty('--height', `${sizes.height}px`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
listener?.({ width: sizes.width, height: sizes.height });
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (modifiers.watch) {
|
|
37
|
+
resizeObservers.set(element, tap(new ResizeObserver(update)).observe(element));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
update();
|
|
41
|
+
},
|
|
42
|
+
unmounted(element) {
|
|
43
|
+
resizeObservers.get(element)?.unobserve(element);
|
|
44
|
+
resizeObservers.delete(element);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { JSError } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
import { defineServiceState } from '@aerogel/core/services';
|
|
4
|
+
|
|
5
|
+
export type ErrorSource = string | Error | JSError | unknown;
|
|
6
|
+
|
|
7
|
+
export interface ErrorReport {
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
details?: string;
|
|
11
|
+
error?: Error | JSError | unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ErrorReportLog {
|
|
15
|
+
report: ErrorReport;
|
|
16
|
+
seen: boolean;
|
|
17
|
+
date: Date;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default defineServiceState({
|
|
21
|
+
name: 'errors',
|
|
22
|
+
initialState: {
|
|
23
|
+
logs: [] as ErrorReportLog[],
|
|
24
|
+
startupErrors: [] as ErrorReport[],
|
|
25
|
+
},
|
|
26
|
+
computed: {
|
|
27
|
+
hasErrors: ({ logs }) => logs.length > 0,
|
|
28
|
+
hasNewErrors: ({ logs }) => logs.some((error) => !error.seen),
|
|
29
|
+
hasStartupErrors: ({ startupErrors }) => startupErrors.length > 0,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { JSError, facade, isDevelopment, isObject, isTesting, objectWithoutEmpty, toString } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
import App from '@aerogel/core/services/App';
|
|
4
|
+
import ServiceBootError from '@aerogel/core/errors/ServiceBootError';
|
|
5
|
+
import UI from '@aerogel/core/ui/UI';
|
|
6
|
+
import { translateWithDefault } from '@aerogel/core/lang/utils';
|
|
7
|
+
import { Events } from '@aerogel/core/services';
|
|
8
|
+
|
|
9
|
+
import Service from './Errors.state';
|
|
10
|
+
import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
11
|
+
|
|
12
|
+
export class ErrorsService extends Service {
|
|
13
|
+
|
|
14
|
+
public forceReporting: boolean = false;
|
|
15
|
+
private enabled: boolean = true;
|
|
16
|
+
|
|
17
|
+
public enable(): void {
|
|
18
|
+
this.enabled = true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public disable(): void {
|
|
22
|
+
this.enabled = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async inspect(error: ErrorSource | ErrorReport, reports?: ErrorReport[]): Promise<void>;
|
|
26
|
+
public async inspect(reports: ErrorReport[]): Promise<void>;
|
|
27
|
+
public async inspect(errorOrReports: ErrorSource | ErrorReport[], _reports?: ErrorReport[]): Promise<void> {
|
|
28
|
+
if (Array.isArray(errorOrReports) && errorOrReports.length === 0) {
|
|
29
|
+
UI.alert(translateWithDefault('errors.inspectEmpty', 'Nothing to inspect!'));
|
|
30
|
+
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const report = Array.isArray(errorOrReports)
|
|
35
|
+
? (errorOrReports[0] as ErrorReport)
|
|
36
|
+
: this.isErrorReport(errorOrReports)
|
|
37
|
+
? errorOrReports
|
|
38
|
+
: await this.createErrorReport(errorOrReports);
|
|
39
|
+
const reports = Array.isArray(errorOrReports) ? (errorOrReports as ErrorReport[]) : (_reports ?? [report]);
|
|
40
|
+
|
|
41
|
+
UI.modal(UI.requireComponent('error-report-modal'), { report, reports });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async report(error: ErrorSource, message?: string): Promise<void> {
|
|
45
|
+
await Events.emit('error', { error, message });
|
|
46
|
+
|
|
47
|
+
if (isTesting('unit')) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isDevelopment()) {
|
|
52
|
+
this.logError(error);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!this.enabled) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!App.isMounted()) {
|
|
60
|
+
const startupError = await this.createStartupErrorReport(error);
|
|
61
|
+
|
|
62
|
+
if (startupError) {
|
|
63
|
+
this.setState({ startupErrors: this.startupErrors.concat(startupError) });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const report = await this.createErrorReport(error);
|
|
70
|
+
const log: ErrorReportLog = {
|
|
71
|
+
report,
|
|
72
|
+
seen: false,
|
|
73
|
+
date: new Date(),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
UI.toast(
|
|
77
|
+
message ??
|
|
78
|
+
translateWithDefault('errors.notice', 'Something went wrong, but it\'s not your fault. Try again!'),
|
|
79
|
+
{
|
|
80
|
+
variant: 'danger',
|
|
81
|
+
actions: [
|
|
82
|
+
{
|
|
83
|
+
label: translateWithDefault('errors.viewDetails', 'View details'),
|
|
84
|
+
dismiss: true,
|
|
85
|
+
click: () => UI.modal(UI.requireComponent('error-report-modal'), { report, reports: [report] }),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
this.setState({ logs: [log].concat(this.logs) });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public reportDevelopmentError(error: ErrorSource, message?: string): void {
|
|
95
|
+
if (!isDevelopment()) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (message) {
|
|
100
|
+
// eslint-disable-next-line no-console
|
|
101
|
+
console.warn(message);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.logError(error);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public see(report: ErrorReport): void {
|
|
108
|
+
this.setState({
|
|
109
|
+
logs: this.logs.map((log) => {
|
|
110
|
+
if (log.report !== report) {
|
|
111
|
+
return log;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
...log,
|
|
116
|
+
seen: true,
|
|
117
|
+
};
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public seeAll(): void {
|
|
123
|
+
this.setState({
|
|
124
|
+
logs: this.logs.map((log) => ({
|
|
125
|
+
...log,
|
|
126
|
+
seen: true,
|
|
127
|
+
})),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private logError(error: unknown): void {
|
|
132
|
+
// eslint-disable-next-line no-console
|
|
133
|
+
console.error(error);
|
|
134
|
+
|
|
135
|
+
if (isObject(error) && error.cause) {
|
|
136
|
+
this.logError(error.cause);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private isErrorReport(error: unknown): error is ErrorReport {
|
|
141
|
+
return isObject(error) && 'title' in error;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async createErrorReport(error: ErrorSource): Promise<ErrorReport> {
|
|
145
|
+
if (typeof error === 'string') {
|
|
146
|
+
return { title: error };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (error instanceof Error || error instanceof JSError) {
|
|
150
|
+
return this.createErrorReportFromError(error);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (isObject(error)) {
|
|
154
|
+
return objectWithoutEmpty({
|
|
155
|
+
title: toString(
|
|
156
|
+
error['name'] ?? error['title'] ?? translateWithDefault('errors.unknown', 'Unknown Error'),
|
|
157
|
+
),
|
|
158
|
+
description: toString(
|
|
159
|
+
error['message'] ??
|
|
160
|
+
error['description'] ??
|
|
161
|
+
translateWithDefault('errors.unknownDescription', 'Unknown error object'),
|
|
162
|
+
),
|
|
163
|
+
error,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
title: translateWithDefault('errors.unknown', 'Unknown Error'),
|
|
169
|
+
error,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async createStartupErrorReport(error: ErrorSource): Promise<ErrorReport | null> {
|
|
174
|
+
if (error instanceof ServiceBootError) {
|
|
175
|
+
// Ignore second-order boot errors in order to have a cleaner startup crash screen.
|
|
176
|
+
return error.cause instanceof ServiceBootError ? null : this.createErrorReport(error.cause);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return this.createErrorReport(error);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private createErrorReportFromError(error: Error | JSError, defaults: Partial<ErrorReport> = {}): ErrorReport {
|
|
183
|
+
return {
|
|
184
|
+
title: error.name,
|
|
185
|
+
description: error.message,
|
|
186
|
+
details: error.stack,
|
|
187
|
+
error,
|
|
188
|
+
...defaults,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export default facade(ErrorsService);
|
|
195
|
+
|
|
196
|
+
declare module '@aerogel/core/services/Events' {
|
|
197
|
+
export interface EventsPayload {
|
|
198
|
+
error: { error: ErrorSource; message?: string };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { App as AppInstance } from 'vue';
|
|
2
|
+
|
|
3
|
+
import App from '@aerogel/core/services/App';
|
|
4
|
+
import { bootServices } from '@aerogel/core/services';
|
|
5
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
6
|
+
|
|
7
|
+
import Errors from './Errors';
|
|
8
|
+
import settings from './settings';
|
|
9
|
+
import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
10
|
+
|
|
11
|
+
export * from './utils';
|
|
12
|
+
export { Errors };
|
|
13
|
+
export { default as JobCancelledError } from './JobCancelledError';
|
|
14
|
+
export { default as ServiceBootError } from './ServiceBootError';
|
|
15
|
+
export type { ErrorSource, ErrorReport, ErrorReportLog };
|
|
16
|
+
|
|
17
|
+
const services = { $errors: Errors };
|
|
18
|
+
const frameworkHandler: ErrorHandler = (error) => {
|
|
19
|
+
Errors.report(error);
|
|
20
|
+
|
|
21
|
+
return true;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function setUpErrorHandler(app: AppInstance, baseHandler: ErrorHandler = () => false): void {
|
|
25
|
+
const errorHandler: ErrorHandler = (error) => baseHandler(error) || frameworkHandler(error);
|
|
26
|
+
|
|
27
|
+
app.config.errorHandler = errorHandler;
|
|
28
|
+
globalThis.onerror = (event, _, __, ___, error) => errorHandler(error ?? event);
|
|
29
|
+
globalThis.onunhandledrejection = (event) => errorHandler(event.reason);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ErrorHandler = (error: ErrorSource) => boolean;
|
|
33
|
+
export type ErrorsServices = typeof services;
|
|
34
|
+
|
|
35
|
+
export default definePlugin({
|
|
36
|
+
async install(app, options) {
|
|
37
|
+
setUpErrorHandler(app, options.handleError);
|
|
38
|
+
|
|
39
|
+
settings.forEach((setting) => App.addSetting(setting));
|
|
40
|
+
|
|
41
|
+
await bootServices(app, services);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
46
|
+
export interface AerogelOptions {
|
|
47
|
+
handleError?(error: ErrorSource): boolean;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
declare module '@aerogel/core/services' {
|
|
52
|
+
export interface Services extends ErrorsServices {}
|
|
53
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Setting
|
|
3
|
+
title-id="debug-setting"
|
|
4
|
+
:title="$td('settings.debug', 'Debugging')"
|
|
5
|
+
:description="$td('settings.debugDescription', 'Enable debugging with [Eruda](https://eruda.liriliri.io/).')"
|
|
6
|
+
>
|
|
7
|
+
<Switch v-model="enabled" aria-labelledby="debug-setting" />
|
|
8
|
+
</Setting>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup lang="ts">
|
|
12
|
+
import { ref, watchEffect } from 'vue';
|
|
13
|
+
import type Eruda from 'eruda';
|
|
14
|
+
|
|
15
|
+
import Setting from '@aerogel/core/components/ui/Setting.vue';
|
|
16
|
+
import Switch from '@aerogel/core/components/ui/Switch.vue';
|
|
17
|
+
|
|
18
|
+
let eruda: typeof Eruda | null = null;
|
|
19
|
+
const enabled = ref(false);
|
|
20
|
+
|
|
21
|
+
watchEffect(async () => {
|
|
22
|
+
if (!enabled.value) {
|
|
23
|
+
eruda?.destroy();
|
|
24
|
+
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
eruda ??= (await import('eruda')).default;
|
|
29
|
+
|
|
30
|
+
eruda.init();
|
|
31
|
+
});
|
|
32
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { JSError, isObject, toString } from '@noeldemartin/utils';
|
|
2
|
+
import { translateWithDefault } from '@aerogel/core/lang/utils';
|
|
3
|
+
import type { ErrorSource } from './Errors.state';
|
|
4
|
+
|
|
5
|
+
const handlers: ErrorHandler[] = [];
|
|
6
|
+
|
|
7
|
+
export type ErrorHandler = (error: ErrorSource) => string | undefined;
|
|
8
|
+
|
|
9
|
+
export function registerErrorHandler(handler: ErrorHandler): void {
|
|
10
|
+
handlers.push(handler);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getErrorMessage(error: ErrorSource): string {
|
|
14
|
+
for (const handler of handlers) {
|
|
15
|
+
const result = handler(error);
|
|
16
|
+
|
|
17
|
+
if (result) {
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof error === 'string') {
|
|
23
|
+
return error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (error instanceof Error || error instanceof JSError) {
|
|
27
|
+
return error.message;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (isObject(error)) {
|
|
31
|
+
return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return translateWithDefault('errors.unknown', 'Unknown Error');
|
|
35
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
2
|
+
import { tt } from '@noeldemartin/testing';
|
|
3
|
+
import type { Equals } from '@noeldemartin/utils';
|
|
4
|
+
import type { Expect } from '@noeldemartin/testing';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
enumInput,
|
|
8
|
+
numberInput,
|
|
9
|
+
objectInput,
|
|
10
|
+
requiredObjectInput,
|
|
11
|
+
requiredStringInput,
|
|
12
|
+
stringInput,
|
|
13
|
+
} from '@aerogel/core/forms/utils';
|
|
14
|
+
import { useForm } from '@aerogel/core/utils/composition/forms';
|
|
15
|
+
|
|
16
|
+
describe('FormController', () => {
|
|
17
|
+
|
|
18
|
+
it('defines magic fields', () => {
|
|
19
|
+
const form = useForm({
|
|
20
|
+
name: requiredStringInput(),
|
|
21
|
+
age: numberInput(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expectTypeOf(form.name).toEqualTypeOf<string>();
|
|
25
|
+
expectTypeOf(form.age).toEqualTypeOf<number | null>();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('validates required fields', () => {
|
|
29
|
+
// Arrange
|
|
30
|
+
const form = useForm({
|
|
31
|
+
name: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
rules: 'required',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Act
|
|
38
|
+
form.submit();
|
|
39
|
+
|
|
40
|
+
// Assert
|
|
41
|
+
expect(form.valid).toBe(false);
|
|
42
|
+
expect(form.submitted).toBe(true);
|
|
43
|
+
expect(form.errors.name).toEqual(['required']);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('resets form', () => {
|
|
47
|
+
// Arrange
|
|
48
|
+
const form = useForm({
|
|
49
|
+
name: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
rules: 'required',
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
form.name = 'Foo bar';
|
|
56
|
+
form.submit();
|
|
57
|
+
|
|
58
|
+
// Act
|
|
59
|
+
form.reset();
|
|
60
|
+
|
|
61
|
+
// Assert
|
|
62
|
+
expect(form.valid).toBe(true);
|
|
63
|
+
expect(form.submitted).toBe(false);
|
|
64
|
+
expect(form.name).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('trims values', () => {
|
|
68
|
+
// Arrange
|
|
69
|
+
const form = useForm({
|
|
70
|
+
trimmed: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
rules: 'required',
|
|
73
|
+
},
|
|
74
|
+
untrimmed: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
rules: 'required',
|
|
77
|
+
trim: false,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
form.trimmed = ' ';
|
|
83
|
+
form.untrimmed = ' ';
|
|
84
|
+
|
|
85
|
+
form.submit();
|
|
86
|
+
|
|
87
|
+
// Assert
|
|
88
|
+
expect(form.valid).toBe(false);
|
|
89
|
+
expect(form.submitted).toBe(true);
|
|
90
|
+
expect(form.trimmed).toEqual('');
|
|
91
|
+
expect(form.untrimmed).toEqual(' ');
|
|
92
|
+
expect(form.errors).toEqual({ trimmed: ['required'], untrimmed: null });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('infers field types', () => {
|
|
96
|
+
const form = useForm({
|
|
97
|
+
one: stringInput(),
|
|
98
|
+
two: requiredStringInput(),
|
|
99
|
+
three: objectInput(),
|
|
100
|
+
four: requiredObjectInput<{ foo: string; bar?: number }>(),
|
|
101
|
+
five: enumInput(['foo', 'bar']),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
tt<
|
|
105
|
+
| Expect<Equals<typeof form.one, string | null>>
|
|
106
|
+
| Expect<Equals<typeof form.two, string>>
|
|
107
|
+
| Expect<Equals<typeof form.three, object | null>>
|
|
108
|
+
| Expect<Equals<typeof form.four, { foo: string; bar?: number }>>
|
|
109
|
+
| Expect<Equals<typeof form.five, 'foo' | 'bar' | null>>
|
|
110
|
+
>();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
});
|