@aerogel/core 0.0.0-next.d824b40e5d06757cd9f47c9f771d916185df4f05 → 0.0.0-next.f0368a3107664018eb88652e719d2cb7d24b82e7

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 (185) hide show
  1. package/dist/aerogel-core.d.ts +2620 -895
  2. package/dist/aerogel-core.js +3296 -0
  3. package/dist/aerogel-core.js.map +1 -0
  4. package/package.json +36 -34
  5. package/src/bootstrap/bootstrap.test.ts +7 -10
  6. package/src/bootstrap/index.ts +43 -15
  7. package/src/bootstrap/options.ts +4 -1
  8. package/src/components/AppLayout.vue +16 -0
  9. package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
  10. package/src/components/AppOverlays.vue +9 -0
  11. package/src/components/AppToasts.vue +16 -0
  12. package/src/components/contracts/AlertModal.ts +4 -0
  13. package/src/components/contracts/Button.ts +16 -0
  14. package/src/components/contracts/ConfirmModal.ts +42 -0
  15. package/src/components/contracts/DropdownMenu.ts +20 -0
  16. package/src/components/contracts/ErrorReportModal.ts +29 -0
  17. package/src/components/contracts/Input.ts +26 -0
  18. package/src/components/contracts/LoadingModal.ts +22 -0
  19. package/src/components/contracts/Modal.ts +21 -0
  20. package/src/components/contracts/PromptModal.ts +31 -0
  21. package/src/components/contracts/Select.ts +45 -0
  22. package/src/components/contracts/Toast.ts +13 -0
  23. package/src/components/contracts/index.ts +11 -0
  24. package/src/components/headless/HeadlessButton.vue +51 -0
  25. package/src/components/headless/HeadlessInput.vue +59 -0
  26. package/src/components/headless/HeadlessInputDescription.vue +27 -0
  27. package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
  28. package/src/components/headless/HeadlessInputInput.vue +75 -0
  29. package/src/components/headless/HeadlessInputLabel.vue +18 -0
  30. package/src/components/headless/HeadlessInputTextArea.vue +40 -0
  31. package/src/components/headless/HeadlessModal.vue +57 -0
  32. package/src/components/headless/HeadlessModalContent.vue +30 -0
  33. package/src/components/headless/HeadlessModalDescription.vue +12 -0
  34. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  35. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  36. package/src/components/headless/HeadlessSelect.vue +120 -0
  37. package/src/components/headless/HeadlessSelectError.vue +25 -0
  38. package/src/components/headless/HeadlessSelectLabel.vue +25 -0
  39. package/src/components/headless/HeadlessSelectOption.vue +34 -0
  40. package/src/components/headless/HeadlessSelectOptions.vue +42 -0
  41. package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
  42. package/src/components/headless/HeadlessSelectValue.vue +18 -0
  43. package/src/components/headless/HeadlessToast.vue +18 -0
  44. package/src/components/headless/HeadlessToastAction.vue +13 -0
  45. package/src/components/headless/index.ts +19 -3
  46. package/src/components/index.ts +6 -9
  47. package/src/components/ui/AdvancedOptions.vue +18 -0
  48. package/src/components/ui/AlertModal.vue +13 -0
  49. package/src/components/ui/Button.vue +98 -0
  50. package/src/components/ui/Checkbox.vue +56 -0
  51. package/src/components/ui/ConfirmModal.vue +42 -0
  52. package/src/components/ui/DropdownMenu.vue +32 -0
  53. package/src/components/ui/DropdownMenuOption.vue +14 -0
  54. package/src/components/ui/DropdownMenuOptions.vue +27 -0
  55. package/src/components/ui/EditableContent.vue +82 -0
  56. package/src/components/ui/ErrorMessage.vue +15 -0
  57. package/src/components/ui/ErrorReportModal.vue +62 -0
  58. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +38 -29
  59. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  60. package/src/components/ui/Form.vue +24 -0
  61. package/src/components/ui/Input.vue +56 -0
  62. package/src/components/ui/Link.vue +12 -0
  63. package/src/components/ui/LoadingModal.vue +32 -0
  64. package/src/components/ui/Markdown.vue +69 -0
  65. package/src/components/ui/Modal.vue +121 -0
  66. package/src/components/ui/ModalContext.vue +30 -0
  67. package/src/components/ui/ProgressBar.vue +51 -0
  68. package/src/components/ui/PromptModal.vue +35 -0
  69. package/src/components/ui/Select.vue +27 -0
  70. package/src/components/ui/SelectLabel.vue +17 -0
  71. package/src/components/ui/SelectOption.vue +29 -0
  72. package/src/components/ui/SelectOptions.vue +35 -0
  73. package/src/components/ui/SelectTrigger.vue +29 -0
  74. package/src/components/ui/SettingsModal.vue +15 -0
  75. package/src/components/ui/StartupCrash.vue +31 -0
  76. package/src/components/ui/Toast.vue +42 -0
  77. package/src/components/ui/index.ts +30 -0
  78. package/src/directives/index.ts +13 -5
  79. package/src/directives/measure.ts +40 -0
  80. package/src/errors/Errors.state.ts +1 -1
  81. package/src/errors/Errors.ts +35 -34
  82. package/src/errors/JobCancelledError.ts +3 -0
  83. package/src/errors/index.ts +19 -29
  84. package/src/errors/utils.ts +35 -0
  85. package/src/forms/FormController.test.ts +112 -0
  86. package/src/forms/FormController.ts +254 -0
  87. package/src/forms/index.ts +3 -2
  88. package/src/forms/utils.ts +57 -5
  89. package/src/forms/validation.ts +19 -0
  90. package/src/index.css +72 -0
  91. package/src/{main.ts → index.ts} +3 -0
  92. package/src/jobs/Job.ts +147 -0
  93. package/src/jobs/index.ts +10 -0
  94. package/src/jobs/listeners.ts +3 -0
  95. package/src/jobs/status.ts +4 -0
  96. package/src/lang/DefaultLangProvider.ts +46 -0
  97. package/src/lang/Lang.state.ts +11 -0
  98. package/src/lang/Lang.ts +44 -29
  99. package/src/lang/index.ts +12 -6
  100. package/src/lang/settings/Language.vue +48 -0
  101. package/src/lang/settings/index.ts +10 -0
  102. package/src/plugins/Plugin.ts +1 -1
  103. package/src/plugins/index.ts +10 -7
  104. package/src/services/App.state.ts +39 -7
  105. package/src/services/App.ts +49 -6
  106. package/src/services/Cache.ts +43 -0
  107. package/src/services/Events.test.ts +39 -0
  108. package/src/services/Events.ts +110 -36
  109. package/src/services/Service.ts +154 -49
  110. package/src/services/Storage.ts +20 -0
  111. package/src/services/index.ts +18 -6
  112. package/src/services/store.ts +8 -5
  113. package/src/services/utils.ts +18 -0
  114. package/src/testing/index.ts +26 -0
  115. package/src/testing/setup.ts +11 -0
  116. package/src/ui/UI.state.ts +21 -7
  117. package/src/ui/UI.ts +277 -72
  118. package/src/ui/index.ts +23 -17
  119. package/src/ui/utils.ts +16 -0
  120. package/src/utils/classes.ts +49 -0
  121. package/src/utils/composition/events.ts +4 -5
  122. package/src/utils/composition/forms.ts +20 -4
  123. package/src/utils/composition/persistent.test.ts +33 -0
  124. package/src/utils/composition/persistent.ts +11 -0
  125. package/src/utils/composition/state.test.ts +47 -0
  126. package/src/utils/composition/state.ts +33 -0
  127. package/src/utils/index.ts +5 -0
  128. package/src/utils/markdown.test.ts +50 -0
  129. package/src/utils/markdown.ts +19 -6
  130. package/src/utils/types.ts +3 -0
  131. package/src/utils/vue.ts +28 -118
  132. package/.eslintrc.js +0 -3
  133. package/dist/aerogel-core.cjs.js +0 -2
  134. package/dist/aerogel-core.cjs.js.map +0 -1
  135. package/dist/aerogel-core.esm.js +0 -2
  136. package/dist/aerogel-core.esm.js.map +0 -1
  137. package/dist/virtual.d.ts +0 -11
  138. package/noeldemartin.config.js +0 -5
  139. package/src/components/AGAppLayout.vue +0 -11
  140. package/src/components/AGAppOverlays.vue +0 -37
  141. package/src/components/AGAppSnackbars.vue +0 -13
  142. package/src/components/basic/AGErrorMessage.vue +0 -16
  143. package/src/components/basic/AGLink.vue +0 -9
  144. package/src/components/basic/AGMarkdown.vue +0 -36
  145. package/src/components/basic/index.ts +0 -5
  146. package/src/components/constants.ts +0 -8
  147. package/src/components/forms/AGButton.vue +0 -44
  148. package/src/components/forms/AGCheckbox.vue +0 -35
  149. package/src/components/forms/AGForm.vue +0 -26
  150. package/src/components/forms/AGInput.vue +0 -36
  151. package/src/components/forms/index.ts +0 -6
  152. package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
  153. package/src/components/headless/forms/AGHeadlessInput.ts +0 -8
  154. package/src/components/headless/forms/AGHeadlessInput.vue +0 -54
  155. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
  156. package/src/components/headless/forms/AGHeadlessInputLabel.vue +0 -16
  157. package/src/components/headless/forms/index.ts +0 -6
  158. package/src/components/headless/modals/AGHeadlessModal.ts +0 -7
  159. package/src/components/headless/modals/AGHeadlessModal.vue +0 -88
  160. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  161. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  162. package/src/components/headless/modals/index.ts +0 -6
  163. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  164. package/src/components/headless/snackbars/index.ts +0 -25
  165. package/src/components/modals/AGAlertModal.vue +0 -25
  166. package/src/components/modals/AGConfirmModal.vue +0 -30
  167. package/src/components/modals/AGErrorReportModal.ts +0 -20
  168. package/src/components/modals/AGErrorReportModal.vue +0 -62
  169. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  170. package/src/components/modals/AGLoadingModal.vue +0 -19
  171. package/src/components/modals/AGModal.ts +0 -10
  172. package/src/components/modals/AGModal.vue +0 -37
  173. package/src/components/modals/AGModalContext.ts +0 -8
  174. package/src/components/modals/AGModalContext.vue +0 -22
  175. package/src/components/modals/AGModalTitle.vue +0 -9
  176. package/src/components/modals/index.ts +0 -23
  177. package/src/components/snackbars/AGSnackbar.vue +0 -42
  178. package/src/components/snackbars/index.ts +0 -3
  179. package/src/directives/initial-focus.ts +0 -11
  180. package/src/forms/Form.test.ts +0 -58
  181. package/src/forms/Form.ts +0 -176
  182. package/src/forms/composition.ts +0 -6
  183. package/src/types/virtual.d.ts +0 -11
  184. package/tsconfig.json +0 -11
  185. package/vite.config.ts +0 -14
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <HeadlessToast :class="renderedClasses">
3
+ <Markdown v-if="message" :text="message" inline />
4
+
5
+ <Button
6
+ v-for="(action, key) of actions"
7
+ :key
8
+ :action
9
+ :variant
10
+ :as="HeadlessToastAction"
11
+ />
12
+ </HeadlessToast>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import type { HTMLAttributes } from 'vue';
17
+
18
+ import Button from '@aerogel/core/components/ui/Button.vue';
19
+ import Markdown from '@aerogel/core/components/ui/Markdown.vue';
20
+ import HeadlessToast from '@aerogel/core/components/headless/HeadlessToast.vue';
21
+ import HeadlessToastAction from '@aerogel/core/components/headless/HeadlessToastAction.vue';
22
+ import { computedVariantClasses } from '@aerogel/core/utils/classes';
23
+ import type { ToastProps } from '@aerogel/core/components/contracts/Toast';
24
+ import type { Variants } from '@aerogel/core/utils/classes';
25
+
26
+ const { class: baseClasses, variant = 'secondary' } = defineProps<ToastProps & { class?: HTMLAttributes['class'] }>();
27
+ const renderedClasses = computedVariantClasses<Variants<Pick<ToastProps, 'variant'>>>(
28
+ { baseClasses, variant },
29
+ {
30
+ baseClasses: 'flex items-center gap-2 rounded-md p-2 ring-1 shadow-lg border-gray-200',
31
+ variants: {
32
+ variant: {
33
+ secondary: 'bg-gray-900 text-white ring-black',
34
+ danger: 'bg-red-50 text-red-900 ring-red-100',
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: 'secondary',
39
+ },
40
+ },
41
+ );
42
+ </script>
@@ -0,0 +1,30 @@
1
+ export { default as AdvancedOptions } from './AdvancedOptions.vue';
2
+ export { default as AlertModal } from './AlertModal.vue';
3
+ export { default as Button } from './Button.vue';
4
+ export { default as Checkbox } from './Checkbox.vue';
5
+ export { default as ConfirmModal } from './ConfirmModal.vue';
6
+ export { default as DropdownMenu } from './DropdownMenu.vue';
7
+ export { default as DropdownMenuOption } from './DropdownMenuOption.vue';
8
+ export { default as DropdownMenuOptions } from './DropdownMenuOptions.vue';
9
+ export { default as EditableContent } from './EditableContent.vue';
10
+ export { default as ErrorMessage } from './ErrorMessage.vue';
11
+ export { default as ErrorReportModal } from './ErrorReportModal.vue';
12
+ export { default as ErrorReportModalButtons } from './ErrorReportModalButtons.vue';
13
+ export { default as ErrorReportModalTitle } from './ErrorReportModalTitle.vue';
14
+ export { default as Form } from './Form.vue';
15
+ export { default as Input } from './Input.vue';
16
+ export { default as Link } from './Link.vue';
17
+ export { default as LoadingModal } from './LoadingModal.vue';
18
+ export { default as Markdown } from './Markdown.vue';
19
+ export { default as Modal } from './Modal.vue';
20
+ export { default as ModalContext } from './ModalContext.vue';
21
+ export { default as ProgressBar } from './ProgressBar.vue';
22
+ export { default as PromptModal } from './PromptModal.vue';
23
+ export { default as Select } from './Select.vue';
24
+ export { default as SelectLabel } from './SelectLabel.vue';
25
+ export { default as SelectOption } from './SelectOption.vue';
26
+ export { default as SelectOptions } from './SelectOptions.vue';
27
+ export { default as SelectTrigger } from './SelectTrigger.vue';
28
+ export { default as SettingsModal } from './SettingsModal.vue';
29
+ export { default as StartupCrash } from './StartupCrash.vue';
30
+ export { default as Toast } from './Toast.vue';
@@ -1,13 +1,15 @@
1
1
  import type { Directive } from 'vue';
2
2
 
3
- import { definePlugin } from '@/plugins';
3
+ import { definePlugin } from '@aerogel/core/plugins';
4
4
 
5
- import initialFocus from './initial-focus';
5
+ import measure from './measure';
6
6
 
7
7
  const builtInDirectives: Record<string, Directive> = {
8
- 'initial-focus': initialFocus,
8
+ measure: measure,
9
9
  };
10
10
 
11
+ export * from './measure';
12
+
11
13
  export default definePlugin({
12
14
  install(app, options) {
13
15
  const directives = {
@@ -21,8 +23,14 @@ export default definePlugin({
21
23
  },
22
24
  });
23
25
 
24
- declare module '@/bootstrap/options' {
25
- interface AerogelOptions {
26
+ declare module '@aerogel/core/bootstrap/options' {
27
+ export interface AerogelOptions {
26
28
  directives?: Record<string, Directive>;
27
29
  }
28
30
  }
31
+
32
+ declare module 'vue' {
33
+ interface ComponentCustomDirectives {
34
+ measure: Directive<string, string>;
35
+ }
36
+ }
@@ -0,0 +1,40 @@
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 interface ElementSize {
7
+ width: number;
8
+ height: number;
9
+ }
10
+
11
+ export type MeasureDirectiveListener = (size: ElementSize) => unknown;
12
+
13
+ export default defineDirective({
14
+ mounted(element: HTMLElement, { value }) {
15
+ // TODO replace with argument when typed properly
16
+ const modifiers = { css: true, watch: true };
17
+
18
+ const listener = typeof value === 'function' ? (value as MeasureDirectiveListener) : null;
19
+ const update = () => {
20
+ const sizes = element.getBoundingClientRect();
21
+
22
+ if (modifiers.css) {
23
+ element.style.setProperty('--width', `${sizes.width}px`);
24
+ element.style.setProperty('--height', `${sizes.height}px`);
25
+ }
26
+
27
+ listener?.({ width: sizes.width, height: sizes.height });
28
+ };
29
+
30
+ if (modifiers.watch) {
31
+ resizeObservers.set(element, tap(new ResizeObserver(update)).observe(element));
32
+ }
33
+
34
+ update();
35
+ },
36
+ unmounted(element) {
37
+ resizeObservers.get(element)?.unobserve(element);
38
+ resizeObservers.delete(element);
39
+ },
40
+ });
@@ -1,6 +1,6 @@
1
1
  import type { JSError } from '@noeldemartin/utils';
2
2
 
3
- import { defineServiceState } from '@/services';
3
+ import { defineServiceState } from '@aerogel/core/services';
4
4
 
5
5
  export type ErrorSource = string | Error | JSError | unknown;
6
6
 
@@ -1,12 +1,14 @@
1
- import { JSError, facade, isObject, objectWithoutEmpty, toString } from '@noeldemartin/utils';
1
+ import { JSError, facade, isDevelopment, isObject, isTesting, objectWithoutEmpty, toString } from '@noeldemartin/utils';
2
2
 
3
- import App from '@/services/App';
4
- import ServiceBootError from '@/errors/ServiceBootError';
5
- import UI, { UIComponents } from '@/ui/UI';
6
- import { translateWithDefault } from '@/lang/utils';
3
+ import App from '@aerogel/core/services/App';
4
+ import ServiceBootError from '@aerogel/core/errors/ServiceBootError';
5
+ import UI, { UIComponents } from '@aerogel/core/ui/UI';
6
+ import { translateWithDefault } from '@aerogel/core/lang/utils';
7
+ import { Events } from '@aerogel/core/services';
8
+ import type { ErrorReportModalProps } from '@aerogel/core/components/contracts/ErrorReportModal';
9
+ import type { ModalComponent } from '@aerogel/core/ui/UI.state';
7
10
 
8
11
  import Service from './Errors.state';
9
- import { Colors } from '@/components/constants';
10
12
  import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
11
13
 
12
14
  export class ErrorsService extends Service {
@@ -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,19 @@ export class ErrorsService extends Service {
31
33
  return;
32
34
  }
33
35
 
34
- UI.openModal(UI.requireComponent(UIComponents.ErrorReportModal), { reports });
36
+ UI.openModal<ModalComponent<ErrorReportModalProps>>(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.development || App.testing) {
42
+ await Events.emit('error', { error, message });
43
+
44
+ if (isTesting('unit')) {
45
+ throw error;
46
+ }
47
+
48
+ if (isDevelopment()) {
39
49
  this.logError(error);
40
50
  }
41
51
 
@@ -43,7 +53,7 @@ export class ErrorsService extends Service {
43
53
  throw error;
44
54
  }
45
55
 
46
- if (!App.isMounted) {
56
+ if (!App.isMounted()) {
47
57
  const startupError = await this.createStartupErrorReport(error);
48
58
 
49
59
  if (startupError) {
@@ -60,19 +70,20 @@ export class ErrorsService extends Service {
60
70
  date: new Date(),
61
71
  };
62
72
 
63
- UI.showSnackbar(
73
+ UI.toast(
64
74
  message ??
65
75
  translateWithDefault('errors.notice', 'Something went wrong, but it\'s not your fault. Try again!'),
66
76
  {
67
- color: Colors.Danger,
77
+ variant: 'danger',
68
78
  actions: [
69
79
  {
70
- text: translateWithDefault('errors.viewDetails', 'View details'),
80
+ label: translateWithDefault('errors.viewDetails', 'View details'),
71
81
  dismiss: true,
72
- handler: () =>
73
- UI.openModal(UI.requireComponent(UIComponents.ErrorReportModal), {
74
- reports: [report],
75
- }),
82
+ click: () =>
83
+ UI.openModal<ModalComponent<ErrorReportModalProps>>(
84
+ UI.requireComponent(UIComponents.ErrorReportModal),
85
+ { reports: [report] },
86
+ ),
76
87
  },
77
88
  ],
78
89
  },
@@ -105,22 +116,6 @@ export class ErrorsService extends Service {
105
116
  });
106
117
  }
107
118
 
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
-
124
119
  private logError(error: unknown): void {
125
120
  // eslint-disable-next-line no-console
126
121
  console.error(error);
@@ -180,4 +175,10 @@ export class ErrorsService extends Service {
180
175
 
181
176
  }
182
177
 
183
- export default facade(new ErrorsService());
178
+ export default facade(ErrorsService);
179
+
180
+ declare module '@aerogel/core/services/Events' {
181
+ export interface EventsPayload {
182
+ error: { error: ErrorSource; message?: string };
183
+ }
184
+ }
@@ -0,0 +1,3 @@
1
+ import { JSError } from '@noeldemartin/utils';
2
+
3
+ export default class JobCancelledError extends JSError {}
@@ -1,38 +1,30 @@
1
- import { tap } from '@noeldemartin/utils';
1
+ import type { App } from 'vue';
2
2
 
3
- import { bootServices } from '@/services';
4
- import { definePlugin } from '@/plugins';
3
+ import { bootServices } from '@aerogel/core/services';
4
+ import { definePlugin } from '@aerogel/core/plugins';
5
5
 
6
6
  import Errors from './Errors';
7
- import { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
7
+ import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
8
8
 
9
- export { Errors, ErrorSource, ErrorReport, ErrorReportLog };
9
+ export * from './utils';
10
+ export { Errors };
11
+ export { default as JobCancelledError } from './JobCancelledError';
12
+ export { default as ServiceBootError } from './ServiceBootError';
13
+ export type { ErrorSource, ErrorReport, ErrorReportLog };
10
14
 
11
15
  const services = { $errors: Errors };
12
16
  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
- }
22
-
23
17
  Errors.report(error);
24
18
 
25
19
  return true;
26
20
  };
27
21
 
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
- );
22
+ function setUpErrorHandler(app: App, baseHandler: ErrorHandler = () => false): void {
23
+ const errorHandler: ErrorHandler = (error) => baseHandler(error) || frameworkHandler(error);
24
+
25
+ app.config.errorHandler = errorHandler;
26
+ globalThis.onerror = (event, _, __, ___, error) => errorHandler(error ?? event);
27
+ globalThis.onunhandledrejection = (event) => errorHandler(event.reason);
36
28
  }
37
29
 
38
30
  export type ErrorHandler = (error: ErrorSource) => boolean;
@@ -40,20 +32,18 @@ export type ErrorsServices = typeof services;
40
32
 
41
33
  export default definePlugin({
42
34
  async install(app, options) {
43
- const errorHandler = setUpErrorHandler(options.handleError);
44
-
45
- app.config.errorHandler = errorHandler;
35
+ setUpErrorHandler(app, options.handleError);
46
36
 
47
37
  await bootServices(app, services);
48
38
  },
49
39
  });
50
40
 
51
- declare module '@/bootstrap/options' {
52
- interface AerogelOptions {
41
+ declare module '@aerogel/core/bootstrap/options' {
42
+ export interface AerogelOptions {
53
43
  handleError?(error: ErrorSource): boolean;
54
44
  }
55
45
  }
56
46
 
57
- declare module '@/services' {
47
+ declare module '@aerogel/core/services' {
58
48
  export interface Services extends ErrorsServices {}
59
49
  }
@@ -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,112 @@
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
+ numberInput,
8
+ objectInput,
9
+ requiredObjectInput,
10
+ requiredStringInput,
11
+ stringInput,
12
+ } from '@aerogel/core/forms/utils';
13
+ import { useForm } from '@aerogel/core/utils/composition/forms';
14
+
15
+ import { FormFieldTypes } from './FormController';
16
+
17
+ describe('FormController', () => {
18
+
19
+ it('defines magic fields', () => {
20
+ const form = useForm({
21
+ name: requiredStringInput(),
22
+ age: numberInput(),
23
+ });
24
+
25
+ expectTypeOf(form.name).toEqualTypeOf<string>();
26
+ expectTypeOf(form.age).toEqualTypeOf<number | null>();
27
+ });
28
+
29
+ it('validates required fields', () => {
30
+ // Arrange
31
+ const form = useForm({
32
+ name: {
33
+ type: FormFieldTypes.String,
34
+ rules: 'required',
35
+ },
36
+ });
37
+
38
+ // Act
39
+ form.submit();
40
+
41
+ // Assert
42
+ expect(form.valid).toBe(false);
43
+ expect(form.submitted).toBe(true);
44
+ expect(form.errors.name).toEqual(['required']);
45
+ });
46
+
47
+ it('resets form', () => {
48
+ // Arrange
49
+ const form = useForm({
50
+ name: {
51
+ type: FormFieldTypes.String,
52
+ rules: 'required',
53
+ },
54
+ });
55
+
56
+ form.name = 'Foo bar';
57
+ form.submit();
58
+
59
+ // Act
60
+ form.reset();
61
+
62
+ // Assert
63
+ expect(form.valid).toBe(true);
64
+ expect(form.submitted).toBe(false);
65
+ expect(form.name).toBeNull();
66
+ });
67
+
68
+ it('trims values', () => {
69
+ // Arrange
70
+ const form = useForm({
71
+ trimmed: {
72
+ type: FormFieldTypes.String,
73
+ rules: 'required',
74
+ },
75
+ untrimmed: {
76
+ type: FormFieldTypes.String,
77
+ rules: 'required',
78
+ trim: false,
79
+ },
80
+ });
81
+
82
+ // Act
83
+ form.trimmed = ' ';
84
+ form.untrimmed = ' ';
85
+
86
+ form.submit();
87
+
88
+ // Assert
89
+ expect(form.valid).toBe(false);
90
+ expect(form.submitted).toBe(true);
91
+ expect(form.trimmed).toEqual('');
92
+ expect(form.untrimmed).toEqual(' ');
93
+ expect(form.errors).toEqual({ trimmed: ['required'], untrimmed: null });
94
+ });
95
+
96
+ it('infers field types', () => {
97
+ const form = useForm({
98
+ one: stringInput(),
99
+ two: requiredStringInput(),
100
+ three: objectInput(),
101
+ four: requiredObjectInput<{ foo: string; bar?: number }>(),
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
+ >();
110
+ });
111
+
112
+ });