@aerogel/core 0.0.0-next.59bf5f7cc06e728d0cf6c00de28f1da48d7d6b8e → 0.0.0-next.88c59e62f64db70aedfbc4c31b5bbc287be44483
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 +498 -118
- 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 +3 -3
- package/src/bootstrap/index.ts +4 -1
- 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/AGErrorMessage.vue +16 -0
- package/src/components/basic/AGLink.vue +9 -0
- package/src/components/basic/AGMarkdown.vue +7 -6
- package/src/components/basic/index.ts +3 -1
- package/src/components/constants.ts +8 -0
- package/src/components/forms/AGButton.vue +25 -15
- package/src/components/forms/index.ts +4 -6
- package/src/components/headless/forms/AGHeadlessButton.vue +7 -7
- package/src/components/headless/forms/AGHeadlessInput.vue +1 -1
- package/src/components/headless/forms/AGHeadlessSelect.ts +31 -0
- package/src/components/headless/forms/AGHeadlessSelect.vue +45 -0
- package/src/components/headless/forms/AGHeadlessSelectButton.ts +3 -0
- package/src/components/headless/forms/AGHeadlessSelectLabel.ts +3 -0
- package/src/components/headless/forms/AGHeadlessSelectOption.ts +8 -0
- package/src/components/headless/forms/AGHeadlessSelectOptions.ts +3 -0
- package/src/components/headless/forms/index.ts +7 -1
- 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/AGAlertModal.vue +0 -1
- package/src/components/modals/AGConfirmModal.vue +3 -3
- package/src/components/modals/AGErrorReportModal.ts +20 -0
- package/src/components/modals/AGErrorReportModal.vue +62 -0
- package/src/components/modals/AGErrorReportModalButtons.vue +109 -0
- package/src/components/modals/AGErrorReportModalTitle.vue +25 -0
- package/src/components/modals/AGModal.ts +1 -1
- package/src/components/modals/AGModal.vue +3 -2
- package/src/components/modals/AGModalTitle.vue +9 -0
- package/src/components/modals/index.ts +17 -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 +60 -9
- package/src/errors/index.ts +40 -2
- package/src/forms/Form.ts +6 -3
- package/src/lang/Lang.ts +1 -1
- package/src/lang/index.ts +1 -1
- package/src/main.ts +0 -2
- package/src/plugins/Plugin.ts +1 -0
- package/src/plugins/index.ts +19 -0
- package/src/services/App.state.ts +8 -3
- package/src/services/App.ts +5 -2
- package/src/services/Service.ts +7 -2
- package/src/services/index.ts +6 -3
- package/src/types/virtual.d.ts +11 -0
- package/src/ui/UI.state.ts +10 -1
- package/src/ui/UI.ts +37 -8
- package/src/ui/index.ts +5 -1
- package/src/utils/markdown.ts +11 -2
- package/src/utils/vue.ts +4 -2
- package/tsconfig.json +1 -0
- package/vite.config.ts +2 -1
- package/src/globals.ts +0 -6
|
@@ -1,8 +1,23 @@
|
|
|
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';
|
|
7
|
+
import AGModalTitle from './AGModalTitle.vue';
|
|
5
8
|
import AGModalContext from './AGModalContext.vue';
|
|
6
|
-
import { IAGModal } from './AGModal';
|
|
7
9
|
|
|
8
|
-
export
|
|
10
|
+
export * from './AGErrorReportModal';
|
|
11
|
+
export * from './AGModal';
|
|
12
|
+
export * from './AGModalContext';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
AGAlertModal,
|
|
16
|
+
AGConfirmModal,
|
|
17
|
+
AGErrorReportModalButtons,
|
|
18
|
+
AGErrorReportModalTitle,
|
|
19
|
+
AGLoadingModal,
|
|
20
|
+
AGModal,
|
|
21
|
+
AGModalTitle,
|
|
22
|
+
AGModalContext,
|
|
23
|
+
};
|
|
@@ -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" inline />
|
|
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 {
|
|
5
|
+
import UI, { UIComponents } from '@/ui/UI';
|
|
6
|
+
import { 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,12 +25,17 @@ 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> {
|
|
32
|
-
if (App.
|
|
38
|
+
if (App.development || App.testing) {
|
|
33
39
|
this.logError(error);
|
|
34
40
|
}
|
|
35
41
|
|
|
@@ -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
|
}
|
|
@@ -84,6 +105,22 @@ export class ErrorsService extends Service {
|
|
|
84
105
|
});
|
|
85
106
|
}
|
|
86
107
|
|
|
108
|
+
public getErrorMessage(error: ErrorSource): string {
|
|
109
|
+
if (typeof error === 'string') {
|
|
110
|
+
return error;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (error instanceof Error || error instanceof JSError) {
|
|
114
|
+
return error.message;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isObject(error)) {
|
|
118
|
+
return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return translateWithDefault('errors.unknown', 'Unknown Error');
|
|
122
|
+
}
|
|
123
|
+
|
|
87
124
|
private logError(error: unknown): void {
|
|
88
125
|
// eslint-disable-next-line no-console
|
|
89
126
|
console.error(error);
|
|
@@ -102,8 +139,22 @@ export class ErrorsService extends Service {
|
|
|
102
139
|
return this.createErrorReportFromError(error);
|
|
103
140
|
}
|
|
104
141
|
|
|
142
|
+
if (isObject(error)) {
|
|
143
|
+
return objectWithoutEmpty({
|
|
144
|
+
title: toString(
|
|
145
|
+
error['name'] ?? error['title'] ?? translateWithDefault('errors.unknown', 'Unknown Error'),
|
|
146
|
+
),
|
|
147
|
+
description: toString(
|
|
148
|
+
error['message'] ??
|
|
149
|
+
error['description'] ??
|
|
150
|
+
translateWithDefault('errors.unknownDescription', 'Unknown error object'),
|
|
151
|
+
),
|
|
152
|
+
error,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
105
156
|
return {
|
|
106
|
-
title:
|
|
157
|
+
title: translateWithDefault('errors.unknown', 'Unknown Error'),
|
|
107
158
|
error,
|
|
108
159
|
};
|
|
109
160
|
}
|
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
|
-
interface Services extends ErrorsServices {}
|
|
58
|
+
export interface Services extends ErrorsServices {}
|
|
21
59
|
}
|
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,6 +37,8 @@ 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
|
|
|
41
44
|
export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
|
|
@@ -92,11 +95,11 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
92
95
|
return this.valid;
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
public reset(): void {
|
|
98
|
+
public reset(options: { keepData?: boolean; keepErrors?: boolean } = {}): void {
|
|
96
99
|
this._submitted.value = false;
|
|
97
100
|
|
|
98
|
-
this.resetData();
|
|
99
|
-
this.resetErrors();
|
|
101
|
+
options.keepData || this.resetData();
|
|
102
|
+
options.keepErrors || this.resetErrors();
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
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
|
},
|
package/src/lang/index.ts
CHANGED
package/src/main.ts
CHANGED
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,13 +1,18 @@
|
|
|
1
|
+
import Build from 'virtual:aerogel';
|
|
2
|
+
|
|
1
3
|
import { defineServiceState } from '@/services/Service';
|
|
4
|
+
import type { Plugin } from '@/plugins/Plugin';
|
|
2
5
|
|
|
3
6
|
export default defineServiceState({
|
|
4
7
|
name: 'app',
|
|
5
8
|
initialState: {
|
|
6
|
-
|
|
9
|
+
plugins: {} as Record<string, Plugin>,
|
|
10
|
+
environment: Build.environment,
|
|
11
|
+
sourceUrl: Build.sourceUrl,
|
|
7
12
|
isMounted: false,
|
|
8
13
|
},
|
|
9
14
|
computed: {
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
development: (state) => state.environment === 'development',
|
|
16
|
+
testing: (state) => state.environment === 'testing',
|
|
12
17
|
},
|
|
13
18
|
});
|
package/src/services/App.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { facade } 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 plugin<T extends Plugin = Plugin>(name: string): T | null {
|
|
11
|
+
return (this.plugins[name] as T) ?? null;
|
|
12
|
+
}
|
|
11
13
|
|
|
14
|
+
protected async boot(): Promise<void> {
|
|
12
15
|
Events.once('application-mounted', () => this.setState({ isMounted: true }));
|
|
13
16
|
}
|
|
14
17
|
|
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) {
|
|
@@ -189,10 +190,14 @@ export default class Service<
|
|
|
189
190
|
return state;
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
protected async
|
|
193
|
+
protected async frameworkBoot(): Promise<void> {
|
|
193
194
|
this.restorePersistedState();
|
|
194
195
|
}
|
|
195
196
|
|
|
197
|
+
protected async boot(): Promise<void> {
|
|
198
|
+
// Override.
|
|
199
|
+
}
|
|
200
|
+
|
|
196
201
|
protected restorePersistedState(): void {
|
|
197
202
|
// TODO fix this.static()
|
|
198
203
|
const persist = (this.constructor as unknown as { persist: string[] }).persist;
|
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({
|
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
|
}
|
|
@@ -105,15 +137,9 @@ export class UIService extends Service {
|
|
|
105
137
|
}
|
|
106
138
|
|
|
107
139
|
protected async boot(): Promise<void> {
|
|
108
|
-
await super.boot();
|
|
109
|
-
|
|
110
140
|
this.watchModalEvents();
|
|
111
141
|
}
|
|
112
142
|
|
|
113
|
-
private requireComponent(name: UIComponent): Component {
|
|
114
|
-
return this.components[name] ?? fail(`UI Component '${name}' is not defined!`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
143
|
private watchModalEvents(): void {
|
|
118
144
|
Events.on('modal-will-close', ({ modal, result }) => {
|
|
119
145
|
this.modalCallbacks[modal.id]?.willClose?.(result);
|
|
@@ -124,7 +150,10 @@ export class UIService extends Service {
|
|
|
124
150
|
});
|
|
125
151
|
|
|
126
152
|
Events.on('modal-closed', async ({ modal, result }) => {
|
|
127
|
-
this.setState(
|
|
153
|
+
this.setState(
|
|
154
|
+
'modals',
|
|
155
|
+
this.modals.filter((m) => m.id !== modal.id),
|
|
156
|
+
);
|
|
128
157
|
|
|
129
158
|
this.modalCallbacks[modal.id]?.closed?.(result);
|
|
130
159
|
|
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({
|
|
@@ -39,5 +43,5 @@ declare module '@/bootstrap/options' {
|
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
declare module '@/services' {
|
|
42
|
-
interface Services extends UIServices {}
|
|
46
|
+
export interface Services extends UIServices {}
|
|
43
47
|
}
|
package/src/utils/markdown.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import { tap } from '@noeldemartin/utils';
|
|
1
2
|
import DOMPurify from 'dompurify';
|
|
2
|
-
import { marked } from 'marked';
|
|
3
|
+
import { Renderer, marked } from 'marked';
|
|
4
|
+
|
|
5
|
+
function makeRenderer(): Renderer {
|
|
6
|
+
return tap(new Renderer(), (renderer) => {
|
|
7
|
+
renderer.link = function(href, title, text) {
|
|
8
|
+
return Renderer.prototype.link.apply(this, [href, title, text]).replace('<a', '<a target="_blank"');
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
}
|
|
3
12
|
|
|
4
13
|
export function renderMarkdown(markdown: string): string {
|
|
5
|
-
return safeHtml(marked(markdown, { mangle: false, headerIds: false }));
|
|
14
|
+
return safeHtml(marked(markdown, { mangle: false, headerIds: false, renderer: makeRenderer() }));
|
|
6
15
|
}
|
|
7
16
|
|
|
8
17
|
export function safeHtml(html: string): string {
|
package/src/utils/vue.ts
CHANGED
|
@@ -3,13 +3,15 @@ import { inject, reactive, ref } from 'vue';
|
|
|
3
3
|
import type { Directive, InjectionKey, PropType, Ref, UnwrapNestedRefs } from 'vue';
|
|
4
4
|
|
|
5
5
|
type BaseProp<T> = {
|
|
6
|
-
type
|
|
6
|
+
type?: PropType<T>;
|
|
7
7
|
validator?(value: unknown): boolean;
|
|
8
8
|
};
|
|
9
9
|
|
|
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[]>,
|
|
@@ -62,7 +64,7 @@ export function injectOrFail<T>(key: InjectionKey<T> | string, errorMessage?: st
|
|
|
62
64
|
return inject(key) ?? fail(errorMessage ?? `Could not resolve '${key}' injection key`);
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
export function mixedProp<T>(type
|
|
67
|
+
export function mixedProp<T>(type?: PropType<T>): OptionalProp<T | null> {
|
|
66
68
|
return {
|
|
67
69
|
type,
|
|
68
70
|
default: null,
|