@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.
Files changed (40) hide show
  1. package/dist/aerogel-core.cjs.js +1 -1
  2. package/dist/aerogel-core.cjs.js.map +1 -1
  3. package/dist/aerogel-core.d.ts +304 -47
  4. package/dist/aerogel-core.esm.js +1 -1
  5. package/dist/aerogel-core.esm.js.map +1 -1
  6. package/dist/virtual.d.ts +11 -0
  7. package/noeldemartin.config.js +4 -1
  8. package/package.json +2 -2
  9. package/src/components/AGAppModals.vue +15 -0
  10. package/src/components/AGAppOverlays.vue +5 -7
  11. package/src/components/AGAppSnackbars.vue +13 -0
  12. package/src/components/basic/AGMarkdown.vue +7 -2
  13. package/src/components/constants.ts +8 -0
  14. package/src/components/forms/AGButton.vue +25 -15
  15. package/src/components/headless/index.ts +1 -0
  16. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +10 -0
  17. package/src/components/headless/snackbars/index.ts +25 -0
  18. package/src/components/index.ts +2 -0
  19. package/src/components/modals/AGConfirmModal.vue +1 -1
  20. package/src/components/modals/AGErrorReportModal.ts +20 -0
  21. package/src/components/modals/AGErrorReportModal.vue +62 -0
  22. package/src/components/modals/AGErrorReportModalButtons.vue +106 -0
  23. package/src/components/modals/AGErrorReportModalTitle.vue +25 -0
  24. package/src/components/modals/AGModal.vue +1 -1
  25. package/src/components/modals/index.ts +15 -2
  26. package/src/components/snackbars/AGSnackbar.vue +42 -0
  27. package/src/components/snackbars/index.ts +3 -0
  28. package/src/directives/index.ts +16 -3
  29. package/src/errors/Errors.ts +36 -7
  30. package/src/errors/index.ts +39 -1
  31. package/src/main.ts +0 -2
  32. package/src/services/App.state.ts +4 -1
  33. package/src/types/virtual.d.ts +11 -0
  34. package/src/ui/UI.state.ts +10 -1
  35. package/src/ui/UI.ts +37 -6
  36. package/src/ui/index.ts +4 -0
  37. package/src/utils/vue.ts +2 -0
  38. package/tsconfig.json +1 -0
  39. package/vite.config.ts +2 -1
  40. 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 { AGAlertModal, AGConfirmModal, AGModal, AGModalContext, AGLoadingModal, IAGModal };
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>
@@ -0,0 +1,3 @@
1
+ import AGSnackbar from './AGSnackbar.vue';
2
+
3
+ export { AGSnackbar };
@@ -4,12 +4,25 @@ import { definePlugin } from '@/plugins';
4
4
 
5
5
  import initialFocus from './initial-focus';
6
6
 
7
- const directives: Record<string, Directive> = {
7
+ const builtInDirectives: Record<string, Directive> = {
8
8
  'initial-focus': initialFocus,
9
9
  };
10
10
 
11
11
  export default definePlugin({
12
- install(app) {
13
- Object.entries(directives).forEach(([name, directive]) => app.directive(name, directive));
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
+ }
@@ -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
- // TODO open errors modal
28
- reports;
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
- // TODO open error snackbar
58
- UI.alert(message ?? 'Something went wrong, but it\'s not your fault! (look at the console for details)');
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,
@@ -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,5 +1,3 @@
1
- import './globals';
2
-
3
1
  export * from './bootstrap';
4
2
  export * from './components';
5
3
  export * from './errors';
@@ -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: __AG_ENV,
8
+ environment: Build.environment,
9
+ sourceUrl: Build.sourceUrl,
7
10
  isMounted: false,
8
11
  },
9
12
  computed: {
@@ -0,0 +1,11 @@
1
+ declare module 'virtual:aerogel' {
2
+ interface AerogelBuild {
3
+ environment: 'production' | 'development' | 'testing';
4
+ basePath?: string;
5
+ sourceUrl?: string;
6
+ }
7
+
8
+ const build: AerogelBuild;
9
+
10
+ export default build;
11
+ }
@@ -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: { modals: [] as Modal[] },
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({ modals: this.modals.filter((m) => m.id !== modal.id) });
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
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "extends": "../../tsconfig.json",
3
3
  "compilerOptions": {
4
+ "types": ["unplugin-icons/types/vue3"],
4
5
  "baseUrl": ".",
5
6
  "paths": {
6
7
  "@/*": ["./src/*"]
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'),
package/src/globals.ts DELETED
@@ -1,6 +0,0 @@
1
- export {};
2
-
3
- declare global {
4
- export const __AG_BASE_PATH: string | undefined;
5
- export const __AG_ENV: 'production' | 'development' | 'testing';
6
- }