@aerogel/core 0.0.0-next.7f6ed5a1f91688a86bf5ede2adc465e4fd6cfdea → 0.0.0-next.824cf5311c4335d119158f507dad094ed4f4f0b6

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 (120) 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 +1630 -204
  4. package/dist/aerogel-core.esm.js +1 -1
  5. package/dist/aerogel-core.esm.js.map +1 -1
  6. package/histoire.config.ts +7 -0
  7. package/noeldemartin.config.js +4 -1
  8. package/package.json +15 -8
  9. package/postcss.config.js +6 -0
  10. package/src/assets/histoire.css +3 -0
  11. package/src/bootstrap/bootstrap.test.ts +4 -59
  12. package/src/bootstrap/index.ts +31 -28
  13. package/src/bootstrap/options.ts +8 -1
  14. package/src/components/AGAppLayout.vue +7 -2
  15. package/src/components/AGAppModals.vue +15 -0
  16. package/src/components/AGAppOverlays.vue +10 -8
  17. package/src/components/AGAppSnackbars.vue +13 -0
  18. package/src/components/constants.ts +8 -0
  19. package/src/components/forms/AGButton.vue +36 -3
  20. package/src/components/forms/AGCheckbox.vue +41 -0
  21. package/src/components/forms/AGInput.vue +15 -9
  22. package/src/components/forms/AGSelect.story.vue +46 -0
  23. package/src/components/forms/AGSelect.vue +60 -0
  24. package/src/components/forms/index.ts +5 -5
  25. package/src/components/headless/forms/AGHeadlessButton.vue +7 -7
  26. package/src/components/headless/forms/AGHeadlessInput.ts +23 -3
  27. package/src/components/headless/forms/AGHeadlessInput.vue +13 -10
  28. package/src/components/headless/forms/AGHeadlessInputError.vue +9 -5
  29. package/src/components/headless/forms/AGHeadlessInputInput.vue +20 -4
  30. package/src/components/headless/forms/AGHeadlessInputLabel.vue +22 -0
  31. package/src/components/headless/forms/AGHeadlessSelect.ts +42 -0
  32. package/src/components/headless/forms/AGHeadlessSelect.vue +77 -0
  33. package/src/components/headless/forms/AGHeadlessSelectButton.vue +24 -0
  34. package/src/components/headless/forms/AGHeadlessSelectError.vue +26 -0
  35. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +24 -0
  36. package/src/components/headless/forms/AGHeadlessSelectOption.ts +4 -0
  37. package/src/components/headless/forms/AGHeadlessSelectOption.vue +39 -0
  38. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +3 -0
  39. package/src/components/headless/forms/index.ts +14 -4
  40. package/src/components/headless/index.ts +1 -0
  41. package/src/components/headless/modals/AGHeadlessModal.ts +27 -0
  42. package/src/components/headless/modals/AGHeadlessModal.vue +8 -6
  43. package/src/components/headless/modals/AGHeadlessModalPanel.vue +10 -2
  44. package/src/components/headless/modals/index.ts +4 -6
  45. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +10 -0
  46. package/src/components/headless/snackbars/index.ts +40 -0
  47. package/src/components/index.ts +5 -2
  48. package/src/components/lib/AGErrorMessage.vue +16 -0
  49. package/src/components/lib/AGLink.vue +9 -0
  50. package/src/components/lib/AGMarkdown.vue +36 -0
  51. package/src/components/lib/AGMeasured.vue +15 -0
  52. package/src/components/lib/AGStartupCrash.vue +31 -0
  53. package/src/components/lib/index.ts +5 -0
  54. package/src/components/modals/AGAlertModal.ts +15 -0
  55. package/src/components/modals/AGAlertModal.vue +4 -5
  56. package/src/components/modals/AGConfirmModal.ts +27 -0
  57. package/src/components/modals/AGConfirmModal.vue +26 -0
  58. package/src/components/modals/AGErrorReportModal.ts +46 -0
  59. package/src/components/modals/AGErrorReportModal.vue +54 -0
  60. package/src/components/modals/AGErrorReportModalButtons.vue +111 -0
  61. package/src/components/modals/AGErrorReportModalTitle.vue +25 -0
  62. package/src/components/modals/AGLoadingModal.ts +23 -0
  63. package/src/components/modals/AGLoadingModal.vue +15 -0
  64. package/src/components/modals/AGModal.ts +5 -1
  65. package/src/components/modals/AGModal.vue +26 -5
  66. package/src/components/modals/AGModalTitle.vue +9 -0
  67. package/src/components/modals/AGPromptModal.ts +30 -0
  68. package/src/components/modals/AGPromptModal.vue +34 -0
  69. package/src/components/modals/index.ts +16 -4
  70. package/src/components/snackbars/AGSnackbar.vue +36 -0
  71. package/src/components/snackbars/index.ts +3 -0
  72. package/src/components/utils.ts +10 -0
  73. package/src/directives/index.ts +21 -4
  74. package/src/directives/measure.ts +12 -0
  75. package/src/errors/Errors.state.ts +31 -0
  76. package/src/errors/Errors.ts +192 -0
  77. package/src/errors/index.ts +45 -0
  78. package/src/forms/Form.test.ts +21 -0
  79. package/src/forms/Form.ts +48 -17
  80. package/src/forms/utils.ts +17 -0
  81. package/src/jobs/Job.ts +5 -0
  82. package/src/jobs/index.ts +7 -0
  83. package/src/lang/Lang.ts +48 -9
  84. package/src/lang/index.ts +17 -76
  85. package/src/lang/utils.ts +4 -0
  86. package/src/main.histoire.ts +1 -0
  87. package/src/main.ts +3 -0
  88. package/src/plugins/Plugin.ts +8 -0
  89. package/src/plugins/index.ts +26 -0
  90. package/src/services/App.state.ts +17 -0
  91. package/src/services/App.ts +47 -0
  92. package/src/services/Events.test.ts +39 -0
  93. package/src/services/Events.ts +88 -30
  94. package/src/services/Service.ts +195 -33
  95. package/src/services/index.ts +33 -7
  96. package/src/services/store.ts +30 -0
  97. package/src/types/vite.d.ts +0 -2
  98. package/src/ui/UI.state.ts +12 -6
  99. package/src/ui/UI.ts +177 -12
  100. package/src/ui/index.ts +31 -16
  101. package/src/utils/composition/events.ts +1 -0
  102. package/src/utils/composition/forms.ts +11 -0
  103. package/src/utils/composition/hooks.ts +9 -0
  104. package/src/utils/index.ts +4 -0
  105. package/src/utils/markdown.ts +11 -2
  106. package/src/utils/tailwindcss.test.ts +26 -0
  107. package/src/utils/tailwindcss.ts +7 -0
  108. package/src/utils/vue.ts +15 -4
  109. package/tailwind.config.js +4 -0
  110. package/tsconfig.json +2 -10
  111. package/vite.config.ts +3 -6
  112. package/.eslintrc.js +0 -3
  113. package/src/bootstrap/hooks.ts +0 -19
  114. package/src/components/basic/AGMarkdown.vue +0 -20
  115. package/src/components/basic/index.ts +0 -3
  116. package/src/lang/helpers.ts +0 -5
  117. package/src/models/index.ts +0 -18
  118. package/src/routing/index.ts +0 -33
  119. package/src/testing/stubs/lang/en.yaml +0 -1
  120. package/src/testing/stubs/models/User.ts +0 -3
@@ -0,0 +1,10 @@
1
+ export function extractComponentProps<T extends Record<string, unknown>>(
2
+ values: Record<string, unknown>,
3
+ definitions: Record<string, unknown>,
4
+ ): T {
5
+ return Object.keys(definitions).reduce((extracted, prop) => {
6
+ extracted[prop] = values[prop];
7
+
8
+ return extracted;
9
+ }, {} as Record<string, unknown>) as T;
10
+ }
@@ -1,13 +1,30 @@
1
1
  import type { Directive } from 'vue';
2
2
 
3
- import { defineBootstrapHook } from '@/bootstrap/hooks';
3
+ import { definePlugin } from '@/plugins';
4
4
 
5
5
  import initialFocus from './initial-focus';
6
+ import measure from './measure';
6
7
 
7
- const directives: Record<string, Directive> = {
8
+ const builtInDirectives: Record<string, Directive> = {
8
9
  'initial-focus': initialFocus,
10
+ 'measure': measure,
9
11
  };
10
12
 
11
- export default defineBootstrapHook(async (app) => {
12
- Object.entries(directives).forEach(([name, directive]) => app.directive(name, directive));
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 '@/bootstrap/options' {
27
+ export interface AerogelOptions {
28
+ directives?: Record<string, Directive>;
29
+ }
30
+ }
@@ -0,0 +1,12 @@
1
+ import { defineDirective } from '@/utils/vue';
2
+
3
+ export default defineDirective({
4
+ mounted(element: HTMLElement, { value }: { value?: () => unknown }) {
5
+ const sizes = element.getBoundingClientRect();
6
+
7
+ element.style.setProperty('--width', `${sizes.width}px`);
8
+ element.style.setProperty('--height', `${sizes.height}px`);
9
+
10
+ value?.();
11
+ },
12
+ });
@@ -0,0 +1,31 @@
1
+ import type { JSError } from '@noeldemartin/utils';
2
+
3
+ import { defineServiceState } from '@/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,192 @@
1
+ import { JSError, facade, isObject, objectWithoutEmpty, toString } from '@noeldemartin/utils';
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';
7
+
8
+ import Service from './Errors.state';
9
+ import { Colors } from '@/components/constants';
10
+ import type { AGErrorReportModalProps } from '@/components/modals/AGErrorReportModal';
11
+ import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
12
+ import type { ModalComponent } from '@/ui/UI.state';
13
+
14
+ export class ErrorsService extends Service {
15
+
16
+ public forceReporting: boolean = false;
17
+ private enabled: boolean = true;
18
+
19
+ public enable(): void {
20
+ this.enabled = true;
21
+ }
22
+
23
+ public disable(): void {
24
+ this.enabled = false;
25
+ }
26
+
27
+ public async inspect(error: ErrorSource | ErrorReport[]): Promise<void> {
28
+ const reports = Array.isArray(error) ? (error as ErrorReport[]) : [await this.createErrorReport(error)];
29
+
30
+ if (reports.length === 0) {
31
+ UI.alert(translateWithDefault('errors.inspectEmpty', 'Nothing to inspect!'));
32
+
33
+ return;
34
+ }
35
+
36
+ UI.openModal<ModalComponent<AGErrorReportModalProps>>(UI.requireComponent(UIComponents.ErrorReportModal), {
37
+ reports,
38
+ });
39
+ }
40
+
41
+ public async report(error: ErrorSource, message?: string): Promise<void> {
42
+ if (App.testing) {
43
+ throw error;
44
+ }
45
+
46
+ if (App.development) {
47
+ this.logError(error);
48
+ }
49
+
50
+ if (!this.enabled) {
51
+ throw error;
52
+ }
53
+
54
+ if (!App.isMounted()) {
55
+ const startupError = await this.createStartupErrorReport(error);
56
+
57
+ if (startupError) {
58
+ this.setState({ startupErrors: this.startupErrors.concat(startupError) });
59
+ }
60
+
61
+ return;
62
+ }
63
+
64
+ const report = await this.createErrorReport(error);
65
+ const log: ErrorReportLog = {
66
+ report,
67
+ seen: false,
68
+ date: new Date(),
69
+ };
70
+
71
+ UI.showSnackbar(
72
+ message ??
73
+ translateWithDefault('errors.notice', 'Something went wrong, but it\'s not your fault. Try again!'),
74
+ {
75
+ color: Colors.Danger,
76
+ actions: [
77
+ {
78
+ text: translateWithDefault('errors.viewDetails', 'View details'),
79
+ dismiss: true,
80
+ handler: () =>
81
+ UI.openModal<ModalComponent<AGErrorReportModalProps>>(
82
+ UI.requireComponent(UIComponents.ErrorReportModal),
83
+ { reports: [report] },
84
+ ),
85
+ },
86
+ ],
87
+ },
88
+ );
89
+
90
+ this.setState({ logs: [log].concat(this.logs) });
91
+ }
92
+
93
+ public see(report: ErrorReport): void {
94
+ this.setState({
95
+ logs: this.logs.map((log) => {
96
+ if (log.report !== report) {
97
+ return log;
98
+ }
99
+
100
+ return {
101
+ ...log,
102
+ seen: true,
103
+ };
104
+ }),
105
+ });
106
+ }
107
+
108
+ public seeAll(): void {
109
+ this.setState({
110
+ logs: this.logs.map((log) => ({
111
+ ...log,
112
+ seen: true,
113
+ })),
114
+ });
115
+ }
116
+
117
+ public getErrorMessage(error: ErrorSource): string {
118
+ if (typeof error === 'string') {
119
+ return error;
120
+ }
121
+
122
+ if (error instanceof Error || error instanceof JSError) {
123
+ return error.message;
124
+ }
125
+
126
+ if (isObject(error)) {
127
+ return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
128
+ }
129
+
130
+ return translateWithDefault('errors.unknown', 'Unknown Error');
131
+ }
132
+
133
+ private logError(error: unknown): void {
134
+ // eslint-disable-next-line no-console
135
+ console.error(error);
136
+
137
+ if (isObject(error) && error.cause) {
138
+ this.logError(error.cause);
139
+ }
140
+ }
141
+
142
+ private async createErrorReport(error: ErrorSource): Promise<ErrorReport> {
143
+ if (typeof error === 'string') {
144
+ return { title: error };
145
+ }
146
+
147
+ if (error instanceof Error || error instanceof JSError) {
148
+ return this.createErrorReportFromError(error);
149
+ }
150
+
151
+ if (isObject(error)) {
152
+ return objectWithoutEmpty({
153
+ title: toString(
154
+ error['name'] ?? error['title'] ?? translateWithDefault('errors.unknown', 'Unknown Error'),
155
+ ),
156
+ description: toString(
157
+ error['message'] ??
158
+ error['description'] ??
159
+ translateWithDefault('errors.unknownDescription', 'Unknown error object'),
160
+ ),
161
+ error,
162
+ });
163
+ }
164
+
165
+ return {
166
+ title: translateWithDefault('errors.unknown', 'Unknown Error'),
167
+ error,
168
+ };
169
+ }
170
+
171
+ private async createStartupErrorReport(error: ErrorSource): Promise<ErrorReport | null> {
172
+ if (error instanceof ServiceBootError) {
173
+ // Ignore second-order boot errors in order to have a cleaner startup crash screen.
174
+ return error.cause instanceof ServiceBootError ? null : this.createErrorReport(error.cause);
175
+ }
176
+
177
+ return this.createErrorReport(error);
178
+ }
179
+
180
+ private createErrorReportFromError(error: Error | JSError, defaults: Partial<ErrorReport> = {}): ErrorReport {
181
+ return {
182
+ title: error.name,
183
+ description: error.message,
184
+ details: error.stack,
185
+ error,
186
+ ...defaults,
187
+ };
188
+ }
189
+
190
+ }
191
+
192
+ export default facade(ErrorsService);
@@ -0,0 +1,45 @@
1
+ import type { App } from 'vue';
2
+
3
+ import { bootServices } from '@/services';
4
+ import { definePlugin } from '@/plugins';
5
+
6
+ import Errors from './Errors';
7
+ import { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
8
+
9
+ export { Errors, ErrorSource, ErrorReport, ErrorReportLog };
10
+
11
+ const services = { $errors: Errors };
12
+ const frameworkHandler: ErrorHandler = (error) => {
13
+ Errors.report(error);
14
+
15
+ return true;
16
+ };
17
+
18
+ function setUpErrorHandler(app: App, baseHandler: ErrorHandler = () => false): void {
19
+ const errorHandler: ErrorHandler = (error) => baseHandler(error) || frameworkHandler(error);
20
+
21
+ app.config.errorHandler = errorHandler;
22
+ globalThis.onerror = (event, _, __, ___, error) => errorHandler(error ?? event);
23
+ globalThis.onunhandledrejection = (event) => errorHandler(event.reason);
24
+ }
25
+
26
+ export type ErrorHandler = (error: ErrorSource) => boolean;
27
+ export type ErrorsServices = typeof services;
28
+
29
+ export default definePlugin({
30
+ async install(app, options) {
31
+ setUpErrorHandler(app, options.handleError);
32
+
33
+ await bootServices(app, services);
34
+ },
35
+ });
36
+
37
+ declare module '@/bootstrap/options' {
38
+ export interface AerogelOptions {
39
+ handleError?(error: ErrorSource): boolean;
40
+ }
41
+ }
42
+
43
+ declare module '@/services' {
44
+ export interface Services extends ErrorsServices {}
45
+ }
@@ -34,4 +34,25 @@ describe('Form', () => {
34
34
  expect(form.errors.name).toEqual(['required']);
35
35
  });
36
36
 
37
+ it('resets form', () => {
38
+ // Arrange
39
+ const form = useForm({
40
+ name: {
41
+ type: FormFieldTypes.String,
42
+ rules: 'required',
43
+ },
44
+ });
45
+
46
+ form.name = 'Foo bar';
47
+ form.submit();
48
+
49
+ // Act
50
+ form.reset();
51
+
52
+ // Assert
53
+ expect(form.valid).toBe(true);
54
+ expect(form.submitted).toBe(false);
55
+ expect(form.name).toBeNull();
56
+ });
57
+
37
58
  });
package/src/forms/Form.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { MagicObject } from '@noeldemartin/utils';
2
- import { reactive, readonly, ref } from 'vue';
2
+ import { computed, reactive, readonly, ref } from 'vue';
3
3
  import type { ObjectValues } from '@noeldemartin/utils';
4
- import type { DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
4
+ import type { ComputedRef, DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
5
5
 
6
6
  export const FormFieldTypes = {
7
7
  String: 'string',
8
8
  Number: 'number',
9
+ Boolean: 'boolean',
10
+ Object: 'object',
9
11
  } as const;
10
12
 
11
13
  export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
@@ -16,9 +18,10 @@ export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType
16
18
 
17
19
  export type FormFieldDefinitions = Record<string, FormFieldDefinition>;
18
20
  export type FormFieldType = ObjectValues<typeof FormFieldTypes>;
21
+ export type FormFieldValue = GetFormFieldValue<FormFieldType>;
19
22
 
20
23
  export type FormData<T> = {
21
- [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
24
+ -readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
22
25
  ? TRules extends 'required'
23
26
  ? GetFormFieldValue<TType>
24
27
  : GetFormFieldValue<TType> | null
@@ -33,15 +36,20 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
33
36
  ? string
34
37
  : TType extends typeof FormFieldTypes.Number
35
38
  ? number
39
+ : TType extends typeof FormFieldTypes.Boolean
40
+ ? boolean
41
+ : TType extends typeof FormFieldTypes.Object
42
+ ? object
36
43
  : never;
37
44
 
45
+ const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
46
+
38
47
  export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
39
48
 
40
49
  public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
41
50
 
42
51
  private _fields: Fields;
43
52
  private _data: FormData<Fields>;
44
- private _valid: Ref<boolean>;
45
53
  private _submitted: Ref<boolean>;
46
54
  private _errors: FormErrors<Fields>;
47
55
 
@@ -50,15 +58,19 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
50
58
 
51
59
  this._fields = fields;
52
60
  this._submitted = ref(false);
53
- this._valid = ref(true);
54
61
  this._data = this.getInitialData(fields);
55
62
  this._errors = this.getInitialErrors(fields);
56
63
 
64
+ validForms.set(
65
+ this,
66
+ computed(() => !Object.values(this._errors).some((error) => error !== null)),
67
+ );
68
+
57
69
  this.errors = readonly(this._errors);
58
70
  }
59
71
 
60
72
  public get valid(): boolean {
61
- return this._valid.value;
73
+ return !!validForms.get(this)?.value;
62
74
  }
63
75
 
64
76
  public get submitted(): boolean {
@@ -78,15 +90,22 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
78
90
  }
79
91
 
80
92
  public validate(): boolean {
81
- const errors = Object.entries(this._fields).reduce((errors, [name, definition]) => {
82
- errors[name] = this.getFieldErrors(name, definition);
93
+ const errors = Object.entries(this._fields).reduce((formErrors, [name, definition]) => {
94
+ formErrors[name] = this.getFieldErrors(name, definition);
83
95
 
84
- return errors;
96
+ return formErrors;
85
97
  }, {} as Record<string, string[] | null>);
86
98
 
87
- Object.assign(this._errors, errors);
99
+ this.resetErrors(errors);
100
+
101
+ return this.valid;
102
+ }
103
+
104
+ public reset(options: { keepData?: boolean; keepErrors?: boolean } = {}): void {
105
+ this._submitted.value = false;
88
106
 
89
- return (this._valid.value = !Object.values(errors).some((error) => error !== null));
107
+ options.keepData || this.resetData();
108
+ options.keepErrors || this.resetErrors();
90
109
  }
91
110
 
92
111
  public submit(): boolean {
@@ -128,10 +147,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
128
147
  return {} as FormData<Fields>;
129
148
  }
130
149
 
131
- const data = Object.entries(fields).reduce((data, [name, definition]) => {
132
- data[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
150
+ const data = Object.entries(fields).reduce((formData, [name, definition]) => {
151
+ formData[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
133
152
 
134
- return data;
153
+ return formData;
135
154
  }, {} as FormData<Fields>);
136
155
 
137
156
  return reactive(data) as FormData<Fields>;
@@ -142,13 +161,25 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
142
161
  return {} as FormErrors<Fields>;
143
162
  }
144
163
 
145
- const errors = Object.keys(fields).reduce((errors, name) => {
146
- errors[name as keyof Fields] = null;
164
+ const errors = Object.keys(fields).reduce((formErrors, name) => {
165
+ formErrors[name as keyof Fields] = null;
147
166
 
148
- return errors;
167
+ return formErrors;
149
168
  }, {} as FormErrors<Fields>);
150
169
 
151
170
  return reactive(errors) as FormErrors<Fields>;
152
171
  }
153
172
 
173
+ private resetData(): void {
174
+ for (const [name, field] of Object.entries(this._fields)) {
175
+ this._data[name as keyof Fields] = (field.default ?? null) as FormData<Fields>[keyof Fields];
176
+ }
177
+ }
178
+
179
+ private resetErrors(errors?: Record<string, string[] | null>): void {
180
+ Object.keys(this._errors).forEach((key) => delete this._errors[key as keyof Fields]);
181
+
182
+ errors && Object.assign(this._errors, errors);
183
+ }
184
+
154
185
  }
@@ -1,6 +1,23 @@
1
1
  import { FormFieldTypes } from './Form';
2
2
  import type { FormFieldDefinition } from './Form';
3
3
 
4
+ export function booleanInput(defaultValue?: boolean): FormFieldDefinition<typeof FormFieldTypes.Boolean> {
5
+ return {
6
+ default: defaultValue,
7
+ type: FormFieldTypes.Boolean,
8
+ };
9
+ }
10
+
11
+ export function requiredBooleanInput(
12
+ defaultValue?: boolean,
13
+ ): FormFieldDefinition<typeof FormFieldTypes.Boolean, 'required'> {
14
+ return {
15
+ default: defaultValue,
16
+ type: FormFieldTypes.Boolean,
17
+ rules: 'required',
18
+ };
19
+ }
20
+
4
21
  export function requiredNumberInput(
5
22
  defaultValue?: number,
6
23
  ): FormFieldDefinition<typeof FormFieldTypes.Number, 'required'> {
@@ -0,0 +1,5 @@
1
+ export default abstract class Job {
2
+
3
+ public abstract run(): Promise<void>;
4
+
5
+ }
@@ -0,0 +1,7 @@
1
+ import Job from './Job';
2
+
3
+ export { Job };
4
+
5
+ export async function dispatch(job: Job): Promise<void> {
6
+ await job.run();
7
+ }
package/src/lang/Lang.ts CHANGED
@@ -1,21 +1,60 @@
1
- import { useI18n } from 'vue-i18n';
2
- import { facade } from '@noeldemartin/utils';
3
- import type { Composer } from 'vue-i18n';
1
+ import { facade, toString } from '@noeldemartin/utils';
4
2
 
3
+ import App from '@/services/App';
5
4
  import Service from '@/services/Service';
6
5
 
6
+ export interface LangProvider {
7
+ translate(key: string, parameters?: Record<string, unknown>): string;
8
+ }
9
+
7
10
  export class LangService extends Service {
8
11
 
9
- private i18n?: Composer;
12
+ private provider: LangProvider;
13
+
14
+ constructor() {
15
+ super();
16
+
17
+ this.provider = {
18
+ translate: (key) => {
19
+ // eslint-disable-next-line no-console
20
+ App.development && console.warn('Lang provider is missing');
21
+
22
+ return key;
23
+ },
24
+ };
25
+ }
10
26
 
11
- public setup(): void {
12
- this.i18n = useI18n();
27
+ public setProvider(provider: LangProvider): void {
28
+ this.provider = provider;
13
29
  }
14
30
 
15
- public translate(key: string, parameters: Record<string, unknown> = {}): string {
16
- return this.i18n?.t(key, parameters) ?? key;
31
+ public translate(key: string, parameters?: Record<string, unknown>): string {
32
+ return this.provider.translate(key, parameters) ?? key;
33
+ }
34
+
35
+ public translateWithDefault(key: string, defaultMessage: string): string;
36
+ public translateWithDefault(key: string, parameters: Record<string, unknown>, defaultMessage: string): string;
37
+ public translateWithDefault(
38
+ key: string,
39
+ defaultMessageOrParameters?: string | Record<string, unknown>,
40
+ defaultMessage?: string,
41
+ ): string {
42
+ defaultMessage ??= defaultMessageOrParameters as string;
43
+
44
+ const parameters = typeof defaultMessageOrParameters === 'string' ? {} : defaultMessageOrParameters ?? {};
45
+ const message = this.provider.translate(key, parameters) ?? key;
46
+
47
+ if (message === key) {
48
+ return Object.entries(parameters).reduce(
49
+ (renderedMessage, [name, value]) =>
50
+ renderedMessage.replace(new RegExp(`\\{\\s*${name}\\s*\\}`, 'g'), toString(value)),
51
+ defaultMessage,
52
+ );
53
+ }
54
+
55
+ return message;
17
56
  }
18
57
 
19
58
  }
20
59
 
21
- export default facade(new LangService());
60
+ export default facade(LangService);