@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.
Files changed (187) hide show
  1. package/dist/aerogel-core.css +1 -0
  2. package/dist/aerogel-core.d.ts +2547 -422
  3. package/dist/aerogel-core.js +3722 -0
  4. package/dist/aerogel-core.js.map +1 -0
  5. package/package.json +39 -37
  6. package/src/bootstrap/bootstrap.test.ts +7 -66
  7. package/src/bootstrap/index.ts +46 -33
  8. package/src/bootstrap/options.ts +8 -1
  9. package/src/components/AppLayout.vue +14 -0
  10. package/src/components/AppModals.vue +14 -0
  11. package/src/components/AppOverlays.vue +9 -0
  12. package/src/components/AppToasts.vue +16 -0
  13. package/src/components/contracts/AlertModal.ts +19 -0
  14. package/src/components/contracts/Button.ts +16 -0
  15. package/src/components/contracts/ConfirmModal.ts +48 -0
  16. package/src/components/contracts/DropdownMenu.ts +25 -0
  17. package/src/components/contracts/ErrorReportModal.ts +33 -0
  18. package/src/components/contracts/Input.ts +26 -0
  19. package/src/components/contracts/LoadingModal.ts +26 -0
  20. package/src/components/contracts/Modal.ts +21 -0
  21. package/src/components/contracts/PromptModal.ts +34 -0
  22. package/src/components/contracts/Select.ts +45 -0
  23. package/src/components/contracts/Toast.ts +15 -0
  24. package/src/components/contracts/index.ts +11 -0
  25. package/src/components/headless/HeadlessButton.vue +51 -0
  26. package/src/components/headless/HeadlessInput.vue +59 -0
  27. package/src/components/headless/HeadlessInputDescription.vue +27 -0
  28. package/src/components/headless/HeadlessInputError.vue +22 -0
  29. package/src/components/headless/HeadlessInputInput.vue +86 -0
  30. package/src/components/headless/HeadlessInputLabel.vue +18 -0
  31. package/src/components/headless/HeadlessInputTextArea.vue +40 -0
  32. package/src/components/headless/HeadlessModal.vue +57 -0
  33. package/src/components/headless/HeadlessModalContent.vue +30 -0
  34. package/src/components/headless/HeadlessModalDescription.vue +12 -0
  35. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  36. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  37. package/src/components/headless/HeadlessSelect.vue +120 -0
  38. package/src/components/headless/HeadlessSelectError.vue +25 -0
  39. package/src/components/headless/HeadlessSelectLabel.vue +25 -0
  40. package/src/components/headless/HeadlessSelectOption.vue +34 -0
  41. package/src/components/headless/HeadlessSelectOptions.vue +42 -0
  42. package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
  43. package/src/components/headless/HeadlessSelectValue.vue +18 -0
  44. package/src/components/headless/HeadlessSwitch.vue +96 -0
  45. package/src/components/headless/HeadlessToast.vue +18 -0
  46. package/src/components/headless/HeadlessToastAction.vue +13 -0
  47. package/src/components/headless/index.ts +20 -2
  48. package/src/components/index.ts +6 -6
  49. package/src/components/ui/AdvancedOptions.vue +18 -0
  50. package/src/components/ui/AlertModal.vue +17 -0
  51. package/src/components/ui/Button.vue +115 -0
  52. package/src/components/ui/Checkbox.vue +56 -0
  53. package/src/components/ui/ConfirmModal.vue +50 -0
  54. package/src/components/ui/DropdownMenu.vue +32 -0
  55. package/src/components/ui/DropdownMenuOption.vue +22 -0
  56. package/src/components/ui/DropdownMenuOptions.vue +44 -0
  57. package/src/components/ui/EditableContent.vue +82 -0
  58. package/src/components/ui/ErrorLogs.vue +19 -0
  59. package/src/components/ui/ErrorLogsModal.vue +48 -0
  60. package/src/components/ui/ErrorMessage.vue +15 -0
  61. package/src/components/ui/ErrorReportModal.vue +73 -0
  62. package/src/components/ui/ErrorReportModalButtons.vue +118 -0
  63. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  64. package/src/components/ui/Form.vue +24 -0
  65. package/src/components/ui/Input.vue +56 -0
  66. package/src/components/ui/Link.vue +12 -0
  67. package/src/components/ui/LoadingModal.vue +34 -0
  68. package/src/components/ui/Markdown.vue +97 -0
  69. package/src/components/ui/Modal.vue +131 -0
  70. package/src/components/ui/ModalContext.vue +31 -0
  71. package/src/components/ui/ProgressBar.vue +51 -0
  72. package/src/components/ui/PromptModal.vue +38 -0
  73. package/src/components/ui/Select.vue +27 -0
  74. package/src/components/ui/SelectLabel.vue +21 -0
  75. package/src/components/ui/SelectOption.vue +29 -0
  76. package/src/components/ui/SelectOptions.vue +35 -0
  77. package/src/components/ui/SelectTrigger.vue +29 -0
  78. package/src/components/ui/Setting.vue +31 -0
  79. package/src/components/ui/SettingsModal.vue +15 -0
  80. package/src/components/ui/StartupCrash.vue +31 -0
  81. package/src/components/ui/Switch.vue +11 -0
  82. package/src/components/ui/TextArea.vue +56 -0
  83. package/src/components/ui/Toast.vue +46 -0
  84. package/src/components/ui/index.ts +35 -0
  85. package/src/directives/index.ts +29 -6
  86. package/src/directives/measure.ts +46 -0
  87. package/src/errors/Errors.state.ts +31 -0
  88. package/src/errors/Errors.ts +200 -0
  89. package/src/errors/JobCancelledError.ts +3 -0
  90. package/src/errors/index.ts +53 -0
  91. package/src/errors/settings/Debug.vue +32 -0
  92. package/src/errors/settings/index.ts +10 -0
  93. package/src/errors/utils.ts +35 -0
  94. package/src/forms/FormController.test.ts +113 -0
  95. package/src/forms/FormController.ts +255 -0
  96. package/src/forms/index.ts +3 -2
  97. package/src/forms/utils.ts +87 -14
  98. package/src/forms/validation.ts +50 -0
  99. package/src/index.css +76 -0
  100. package/src/{main.ts → index.ts} +5 -0
  101. package/src/jobs/Job.ts +147 -0
  102. package/src/jobs/index.ts +10 -0
  103. package/src/jobs/listeners.ts +3 -0
  104. package/src/jobs/status.ts +4 -0
  105. package/src/lang/DefaultLangProvider.ts +46 -0
  106. package/src/lang/Lang.state.ts +11 -0
  107. package/src/lang/Lang.ts +63 -9
  108. package/src/lang/index.ts +22 -75
  109. package/src/lang/settings/Language.vue +48 -0
  110. package/src/lang/settings/index.ts +10 -0
  111. package/src/lang/utils.ts +4 -0
  112. package/src/plugins/Plugin.ts +8 -0
  113. package/src/plugins/index.ts +29 -0
  114. package/src/services/App.state.ts +50 -0
  115. package/src/services/App.ts +63 -0
  116. package/src/services/Cache.ts +43 -0
  117. package/src/services/Events.test.ts +39 -0
  118. package/src/services/Events.ts +110 -36
  119. package/src/services/Service.ts +273 -35
  120. package/src/services/Storage.ts +20 -0
  121. package/src/services/index.ts +45 -8
  122. package/src/services/store.ts +30 -0
  123. package/src/services/utils.ts +18 -0
  124. package/src/testing/index.ts +30 -0
  125. package/src/testing/setup.ts +11 -0
  126. package/src/types/vite.d.ts +0 -2
  127. package/src/ui/UI.state.ts +21 -13
  128. package/src/ui/UI.ts +350 -53
  129. package/src/ui/index.ts +40 -25
  130. package/src/ui/utils.ts +16 -0
  131. package/src/utils/app.ts +7 -0
  132. package/src/utils/classes.ts +41 -0
  133. package/src/utils/composition/events.ts +4 -5
  134. package/src/utils/composition/forms.ts +27 -0
  135. package/src/utils/composition/hooks.ts +9 -0
  136. package/src/utils/composition/persistent.test.ts +33 -0
  137. package/src/utils/composition/persistent.ts +11 -0
  138. package/src/utils/composition/state.test.ts +47 -0
  139. package/src/utils/composition/state.ts +33 -0
  140. package/src/utils/index.ts +9 -0
  141. package/src/utils/markdown.test.ts +50 -0
  142. package/src/utils/markdown.ts +60 -4
  143. package/src/utils/types.ts +3 -0
  144. package/src/utils/vue.ts +38 -121
  145. package/.eslintrc.js +0 -3
  146. package/dist/aerogel-core.cjs.js +0 -2
  147. package/dist/aerogel-core.cjs.js.map +0 -1
  148. package/dist/aerogel-core.esm.js +0 -2
  149. package/dist/aerogel-core.esm.js.map +0 -1
  150. package/noeldemartin.config.js +0 -2
  151. package/src/bootstrap/hooks.ts +0 -19
  152. package/src/components/AGAppLayout.vue +0 -11
  153. package/src/components/AGAppOverlays.vue +0 -39
  154. package/src/components/basic/AGMarkdown.vue +0 -20
  155. package/src/components/basic/index.ts +0 -3
  156. package/src/components/forms/AGButton.vue +0 -11
  157. package/src/components/forms/AGForm.vue +0 -26
  158. package/src/components/forms/AGInput.vue +0 -32
  159. package/src/components/forms/index.ts +0 -5
  160. package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
  161. package/src/components/headless/forms/AGHeadlessInput.ts +0 -8
  162. package/src/components/headless/forms/AGHeadlessInput.vue +0 -54
  163. package/src/components/headless/forms/AGHeadlessInputError.vue +0 -22
  164. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -29
  165. package/src/components/headless/forms/index.ts +0 -4
  166. package/src/components/headless/modals/AGHeadlessModal.ts +0 -7
  167. package/src/components/headless/modals/AGHeadlessModal.vue +0 -84
  168. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -20
  169. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  170. package/src/components/headless/modals/index.ts +0 -6
  171. package/src/components/modals/AGAlertModal.vue +0 -15
  172. package/src/components/modals/AGModal.ts +0 -6
  173. package/src/components/modals/AGModal.vue +0 -18
  174. package/src/components/modals/AGModalContext.ts +0 -8
  175. package/src/components/modals/AGModalContext.vue +0 -22
  176. package/src/components/modals/index.ts +0 -5
  177. package/src/directives/initial-focus.ts +0 -11
  178. package/src/forms/Form.test.ts +0 -37
  179. package/src/forms/Form.ts +0 -154
  180. package/src/forms/composition.ts +0 -6
  181. package/src/lang/helpers.ts +0 -5
  182. package/src/models/index.ts +0 -18
  183. package/src/routing/index.ts +0 -33
  184. package/src/testing/stubs/lang/en.yaml +0 -1
  185. package/src/testing/stubs/models/User.ts +0 -3
  186. package/tsconfig.json +0 -19
  187. package/vite.config.ts +0 -17
@@ -1,13 +1,36 @@
1
1
  import type { Directive } from 'vue';
2
2
 
3
- import { defineBootstrapHook } from '@/bootstrap/hooks';
3
+ import { definePlugin } from '@aerogel/core/plugins';
4
4
 
5
- import initialFocus from './initial-focus';
5
+ import measure from './measure';
6
6
 
7
- const directives: Record<string, Directive> = {
8
- 'initial-focus': initialFocus,
7
+ const builtInDirectives: Record<string, Directive> = {
8
+ measure: measure,
9
9
  };
10
10
 
11
- export default defineBootstrapHook(async (app) => {
12
- Object.entries(directives).forEach(([name, directive]) => app.directive(name, directive));
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,3 @@
1
+ import { JSError } from '@noeldemartin/utils';
2
+
3
+ export default class JobCancelledError extends JSError {}
@@ -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,10 @@
1
+ import { defineSettings } from '@aerogel/core/services';
2
+
3
+ import Debug from './Debug.vue';
4
+
5
+ export default defineSettings([
6
+ {
7
+ priority: 10,
8
+ component: Debug,
9
+ },
10
+ ]);
@@ -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
+ });