@aerogel/core 0.0.0-next.7f6ed5a1f91688a86bf5ede2adc465e4fd6cfdea → 0.0.0-next.88c59e62f64db70aedfbc4c31b5bbc287be44483
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aerogel-core.cjs.js +1 -1
- package/dist/aerogel-core.cjs.js.map +1 -1
- package/dist/aerogel-core.d.ts +830 -104
- package/dist/aerogel-core.esm.js +1 -1
- package/dist/aerogel-core.esm.js.map +1 -1
- package/dist/virtual.d.ts +11 -0
- package/noeldemartin.config.js +4 -1
- package/package.json +7 -9
- package/src/bootstrap/bootstrap.test.ts +0 -56
- package/src/bootstrap/index.ts +12 -25
- package/src/bootstrap/options.ts +5 -1
- package/src/components/AGAppModals.vue +15 -0
- package/src/components/AGAppOverlays.vue +5 -7
- package/src/components/AGAppSnackbars.vue +13 -0
- package/src/components/basic/AGErrorMessage.vue +16 -0
- package/src/components/basic/AGLink.vue +9 -0
- package/src/components/basic/AGMarkdown.vue +21 -5
- package/src/components/basic/index.ts +3 -1
- package/src/components/constants.ts +8 -0
- package/src/components/forms/AGButton.vue +36 -3
- package/src/components/forms/AGCheckbox.vue +35 -0
- package/src/components/forms/AGInput.vue +8 -4
- package/src/components/forms/index.ts +4 -5
- package/src/components/headless/forms/AGHeadlessButton.vue +7 -7
- package/src/components/headless/forms/AGHeadlessInput.ts +2 -2
- package/src/components/headless/forms/AGHeadlessInput.vue +6 -6
- package/src/components/headless/forms/AGHeadlessInputError.vue +9 -5
- package/src/components/headless/forms/AGHeadlessInputInput.vue +20 -4
- package/src/components/headless/forms/AGHeadlessInputLabel.vue +16 -0
- package/src/components/headless/forms/AGHeadlessSelect.ts +31 -0
- package/src/components/headless/forms/AGHeadlessSelect.vue +45 -0
- package/src/components/headless/forms/AGHeadlessSelectButton.ts +3 -0
- package/src/components/headless/forms/AGHeadlessSelectLabel.ts +3 -0
- package/src/components/headless/forms/AGHeadlessSelectOption.ts +8 -0
- package/src/components/headless/forms/AGHeadlessSelectOptions.ts +3 -0
- package/src/components/headless/forms/index.ts +12 -4
- package/src/components/headless/index.ts +1 -0
- package/src/components/headless/modals/AGHeadlessModal.vue +5 -1
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +10 -2
- package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +10 -0
- package/src/components/headless/snackbars/index.ts +25 -0
- package/src/components/index.ts +4 -1
- package/src/components/modals/AGAlertModal.vue +12 -2
- package/src/components/modals/AGConfirmModal.vue +30 -0
- package/src/components/modals/AGErrorReportModal.ts +20 -0
- package/src/components/modals/AGErrorReportModal.vue +62 -0
- package/src/components/modals/AGErrorReportModalButtons.vue +109 -0
- package/src/components/modals/AGErrorReportModalTitle.vue +25 -0
- package/src/components/modals/AGLoadingModal.vue +19 -0
- package/src/components/modals/AGModal.ts +4 -0
- package/src/components/modals/AGModal.vue +23 -4
- package/src/components/modals/AGModalTitle.vue +9 -0
- package/src/components/modals/index.ts +20 -2
- package/src/components/snackbars/AGSnackbar.vue +42 -0
- package/src/components/snackbars/index.ts +3 -0
- package/src/directives/index.ts +19 -4
- package/src/errors/Errors.state.ts +31 -0
- package/src/errors/Errors.ts +183 -0
- package/src/errors/index.ts +59 -0
- package/src/forms/Form.test.ts +21 -0
- package/src/forms/Form.ts +41 -16
- package/src/forms/utils.ts +17 -0
- package/src/lang/Lang.ts +47 -8
- package/src/lang/index.ts +17 -76
- package/src/lang/utils.ts +4 -0
- package/src/main.ts +2 -0
- package/src/plugins/Plugin.ts +8 -0
- package/src/plugins/index.ts +26 -0
- package/src/services/App.state.ts +18 -0
- package/src/services/App.ts +20 -0
- package/src/services/Service.ts +157 -29
- package/src/services/index.ts +32 -7
- package/src/services/store.ts +27 -0
- package/src/types/virtual.d.ts +11 -0
- package/src/types/vite.d.ts +0 -2
- package/src/ui/UI.state.ts +12 -6
- package/src/ui/UI.ts +72 -10
- package/src/ui/index.ts +24 -14
- package/src/utils/composition/forms.ts +11 -0
- package/src/utils/composition/hooks.ts +9 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/markdown.ts +11 -2
- package/src/utils/vue.ts +4 -2
- package/tsconfig.json +2 -10
- package/vite.config.ts +3 -6
- package/src/bootstrap/hooks.ts +0 -19
- package/src/lang/helpers.ts +0 -5
- package/src/models/index.ts +0 -18
- package/src/routing/index.ts +0 -33
- package/src/testing/stubs/lang/en.yaml +0 -1
- package/src/testing/stubs/models/User.ts +0 -3
|
@@ -0,0 +1,183 @@
|
|
|
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 { 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[]): Promise<void> {
|
|
26
|
+
const reports = Array.isArray(error) ? error : [await this.createErrorReport(error)];
|
|
27
|
+
|
|
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 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public async report(error: ErrorSource, message?: string): Promise<void> {
|
|
38
|
+
if (App.development || App.testing) {
|
|
39
|
+
this.logError(error);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!this.enabled) {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!App.isMounted) {
|
|
47
|
+
const startupError = await this.createStartupErrorReport(error);
|
|
48
|
+
|
|
49
|
+
if (startupError) {
|
|
50
|
+
this.setState({ startupErrors: this.startupErrors.concat(startupError) });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const report = await this.createErrorReport(error);
|
|
57
|
+
const log: ErrorReportLog = {
|
|
58
|
+
report,
|
|
59
|
+
seen: false,
|
|
60
|
+
date: new Date(),
|
|
61
|
+
};
|
|
62
|
+
|
|
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
|
+
);
|
|
80
|
+
|
|
81
|
+
this.setState({ logs: [log].concat(this.logs) });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public see(report: ErrorReport): void {
|
|
85
|
+
this.setState({
|
|
86
|
+
logs: this.logs.map((log) => {
|
|
87
|
+
if (log.report !== report) {
|
|
88
|
+
return log;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
...log,
|
|
93
|
+
seen: true,
|
|
94
|
+
};
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public seeAll(): void {
|
|
100
|
+
this.setState({
|
|
101
|
+
logs: this.logs.map((log) => ({
|
|
102
|
+
...log,
|
|
103
|
+
seen: true,
|
|
104
|
+
})),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public getErrorMessage(error: ErrorSource): string {
|
|
109
|
+
if (typeof error === 'string') {
|
|
110
|
+
return error;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (error instanceof Error || error instanceof JSError) {
|
|
114
|
+
return error.message;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isObject(error)) {
|
|
118
|
+
return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return translateWithDefault('errors.unknown', 'Unknown Error');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private logError(error: unknown): void {
|
|
125
|
+
// eslint-disable-next-line no-console
|
|
126
|
+
console.error(error);
|
|
127
|
+
|
|
128
|
+
if (isObject(error) && error.cause) {
|
|
129
|
+
this.logError(error.cause);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async createErrorReport(error: ErrorSource): Promise<ErrorReport> {
|
|
134
|
+
if (typeof error === 'string') {
|
|
135
|
+
return { title: error };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (error instanceof Error || error instanceof JSError) {
|
|
139
|
+
return this.createErrorReportFromError(error);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (isObject(error)) {
|
|
143
|
+
return objectWithoutEmpty({
|
|
144
|
+
title: toString(
|
|
145
|
+
error['name'] ?? error['title'] ?? translateWithDefault('errors.unknown', 'Unknown Error'),
|
|
146
|
+
),
|
|
147
|
+
description: toString(
|
|
148
|
+
error['message'] ??
|
|
149
|
+
error['description'] ??
|
|
150
|
+
translateWithDefault('errors.unknownDescription', 'Unknown error object'),
|
|
151
|
+
),
|
|
152
|
+
error,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
title: translateWithDefault('errors.unknown', 'Unknown Error'),
|
|
158
|
+
error,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private async createStartupErrorReport(error: ErrorSource): Promise<ErrorReport | null> {
|
|
163
|
+
if (error instanceof ServiceBootError) {
|
|
164
|
+
// Ignore second-order boot errors in order to have a cleaner startup crash screen.
|
|
165
|
+
return error.cause instanceof ServiceBootError ? null : this.createErrorReport(error.cause);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return this.createErrorReport(error);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private createErrorReportFromError(error: Error | JSError, defaults: Partial<ErrorReport> = {}): ErrorReport {
|
|
172
|
+
return {
|
|
173
|
+
title: error.name,
|
|
174
|
+
description: error.message,
|
|
175
|
+
details: error.stack,
|
|
176
|
+
error,
|
|
177
|
+
...defaults,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export default facade(new ErrorsService());
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { tap } from '@noeldemartin/utils';
|
|
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
|
+
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
|
+
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;
|
|
39
|
+
export type ErrorsServices = typeof services;
|
|
40
|
+
|
|
41
|
+
export default definePlugin({
|
|
42
|
+
async install(app, options) {
|
|
43
|
+
const errorHandler = setUpErrorHandler(options.handleError);
|
|
44
|
+
|
|
45
|
+
app.config.errorHandler = errorHandler;
|
|
46
|
+
|
|
47
|
+
await bootServices(app, services);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
declare module '@/bootstrap/options' {
|
|
52
|
+
interface AerogelOptions {
|
|
53
|
+
handleError?(error: ErrorSource): boolean;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare module '@/services' {
|
|
58
|
+
export interface Services extends ErrorsServices {}
|
|
59
|
+
}
|
package/src/forms/Form.test.ts
CHANGED
|
@@ -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> {
|
|
@@ -18,7 +20,7 @@ export type FormFieldDefinitions = Record<string, FormFieldDefinition>;
|
|
|
18
20
|
export type FormFieldType = ObjectValues<typeof FormFieldTypes>;
|
|
19
21
|
|
|
20
22
|
export type FormData<T> = {
|
|
21
|
-
[k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
|
|
23
|
+
-readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
|
|
22
24
|
? TRules extends 'required'
|
|
23
25
|
? GetFormFieldValue<TType>
|
|
24
26
|
: GetFormFieldValue<TType> | null
|
|
@@ -33,6 +35,10 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
|
|
|
33
35
|
? string
|
|
34
36
|
: TType extends typeof FormFieldTypes.Number
|
|
35
37
|
? number
|
|
38
|
+
: TType extends typeof FormFieldTypes.Boolean
|
|
39
|
+
? boolean
|
|
40
|
+
: TType extends typeof FormFieldTypes.Object
|
|
41
|
+
? object
|
|
36
42
|
: never;
|
|
37
43
|
|
|
38
44
|
export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
|
|
@@ -41,7 +47,7 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
41
47
|
|
|
42
48
|
private _fields: Fields;
|
|
43
49
|
private _data: FormData<Fields>;
|
|
44
|
-
private _valid:
|
|
50
|
+
private _valid: ComputedRef<boolean>;
|
|
45
51
|
private _submitted: Ref<boolean>;
|
|
46
52
|
private _errors: FormErrors<Fields>;
|
|
47
53
|
|
|
@@ -50,9 +56,9 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
50
56
|
|
|
51
57
|
this._fields = fields;
|
|
52
58
|
this._submitted = ref(false);
|
|
53
|
-
this._valid = ref(true);
|
|
54
59
|
this._data = this.getInitialData(fields);
|
|
55
60
|
this._errors = this.getInitialErrors(fields);
|
|
61
|
+
this._valid = computed(() => !Object.values(this._errors).some((error) => error !== null));
|
|
56
62
|
|
|
57
63
|
this.errors = readonly(this._errors);
|
|
58
64
|
}
|
|
@@ -78,15 +84,22 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
public validate(): boolean {
|
|
81
|
-
const errors = Object.entries(this._fields).reduce((
|
|
82
|
-
|
|
87
|
+
const errors = Object.entries(this._fields).reduce((formErrors, [name, definition]) => {
|
|
88
|
+
formErrors[name] = this.getFieldErrors(name, definition);
|
|
83
89
|
|
|
84
|
-
return
|
|
90
|
+
return formErrors;
|
|
85
91
|
}, {} as Record<string, string[] | null>);
|
|
86
92
|
|
|
87
|
-
|
|
93
|
+
this.resetErrors(errors);
|
|
88
94
|
|
|
89
|
-
return
|
|
95
|
+
return this.valid;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public reset(options: { keepData?: boolean; keepErrors?: boolean } = {}): void {
|
|
99
|
+
this._submitted.value = false;
|
|
100
|
+
|
|
101
|
+
options.keepData || this.resetData();
|
|
102
|
+
options.keepErrors || this.resetErrors();
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
public submit(): boolean {
|
|
@@ -128,10 +141,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
128
141
|
return {} as FormData<Fields>;
|
|
129
142
|
}
|
|
130
143
|
|
|
131
|
-
const data = Object.entries(fields).reduce((
|
|
132
|
-
|
|
144
|
+
const data = Object.entries(fields).reduce((formData, [name, definition]) => {
|
|
145
|
+
formData[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
|
|
133
146
|
|
|
134
|
-
return
|
|
147
|
+
return formData;
|
|
135
148
|
}, {} as FormData<Fields>);
|
|
136
149
|
|
|
137
150
|
return reactive(data) as FormData<Fields>;
|
|
@@ -142,13 +155,25 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
142
155
|
return {} as FormErrors<Fields>;
|
|
143
156
|
}
|
|
144
157
|
|
|
145
|
-
const errors = Object.keys(fields).reduce((
|
|
146
|
-
|
|
158
|
+
const errors = Object.keys(fields).reduce((formErrors, name) => {
|
|
159
|
+
formErrors[name as keyof Fields] = null;
|
|
147
160
|
|
|
148
|
-
return
|
|
161
|
+
return formErrors;
|
|
149
162
|
}, {} as FormErrors<Fields>);
|
|
150
163
|
|
|
151
164
|
return reactive(errors) as FormErrors<Fields>;
|
|
152
165
|
}
|
|
153
166
|
|
|
167
|
+
private resetData(): void {
|
|
168
|
+
for (const [name, field] of Object.entries(this._fields)) {
|
|
169
|
+
this._data[name as keyof Fields] = (field.default ?? null) as FormData<Fields>[keyof Fields];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private resetErrors(errors?: Record<string, string[] | null>): void {
|
|
174
|
+
Object.keys(this._errors).forEach((key) => delete this._errors[key as keyof Fields]);
|
|
175
|
+
|
|
176
|
+
errors && Object.assign(this._errors, errors);
|
|
177
|
+
}
|
|
178
|
+
|
|
154
179
|
}
|
package/src/forms/utils.ts
CHANGED
|
@@ -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'> {
|
package/src/lang/Lang.ts
CHANGED
|
@@ -1,19 +1,58 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
12
|
-
this.
|
|
27
|
+
public setProvider(provider: LangProvider): void {
|
|
28
|
+
this.provider = provider;
|
|
13
29
|
}
|
|
14
30
|
|
|
15
|
-
public translate(key: string, parameters
|
|
16
|
-
return this.
|
|
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
|
}
|
package/src/lang/index.ts
CHANGED
|
@@ -1,89 +1,30 @@
|
|
|
1
|
-
import { createI18n } from 'vue-i18n';
|
|
2
|
-
import { fail, stringMatch } from '@noeldemartin/utils';
|
|
3
|
-
import type { I18nOptions } from 'vue-i18n';
|
|
4
|
-
import type { Plugin } from 'vue';
|
|
5
|
-
|
|
6
|
-
import { defineBootstrapHook, onAppMounted } from '@/bootstrap/hooks';
|
|
7
1
|
import { bootServices } from '@/services';
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
import Lang from './Lang';
|
|
11
|
-
|
|
12
|
-
const services = { $lang: Lang };
|
|
13
|
-
|
|
14
|
-
function getLangOptions(options: BootstrapOptions): LangOptions | null {
|
|
15
|
-
if (options.lang) {
|
|
16
|
-
return options.lang;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (options.langMessages) {
|
|
20
|
-
return { messages: options.langMessages };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
2
|
+
import { definePlugin } from '@/plugins';
|
|
25
3
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const locale = stringMatch<2>(fileName, /.*\/lang\/(.+)\.yaml/)?.[1];
|
|
4
|
+
import Lang, { LangProvider } from './Lang';
|
|
5
|
+
import { translate, translateWithDefault } from './utils';
|
|
29
6
|
|
|
30
|
-
|
|
31
|
-
loaders[locale] = () =>
|
|
32
|
-
(loader as () => Promise<{ default: Record<string, unknown> }>)().then(
|
|
33
|
-
({ default: messages }) => messages,
|
|
34
|
-
);
|
|
35
|
-
}
|
|
7
|
+
export { Lang, LangProvider, translate, translateWithDefault };
|
|
36
8
|
|
|
37
|
-
|
|
38
|
-
}, {} as Record<string, LazyMessages>);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function createAppI18n(options: LangOptions): Promise<Plugin> {
|
|
42
|
-
const locale = options.defaultLocale ?? 'en';
|
|
43
|
-
const fallbackLocale = options.fallbackLocale ?? 'en';
|
|
44
|
-
const messageLoaders = getMessageLoaders(options.messages);
|
|
45
|
-
const lazyMessages = messageLoaders[locale] ?? fail<LazyMessages>(`Missing messages for '${locale}' locale`);
|
|
46
|
-
const messages = { [locale]: await lazyMessages() } as I18nOptions['messages'];
|
|
47
|
-
|
|
48
|
-
return createI18n({ locale, fallbackLocale, messages });
|
|
49
|
-
}
|
|
9
|
+
const services = { $lang: Lang };
|
|
50
10
|
|
|
51
11
|
export type LangServices = typeof services;
|
|
52
12
|
|
|
53
|
-
export
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
defaultLocale?: string;
|
|
58
|
-
fallbackLocale?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export * from './helpers';
|
|
62
|
-
export { Lang };
|
|
63
|
-
|
|
64
|
-
export default defineBootstrapHook(async (app, options) => {
|
|
65
|
-
const langOptions = getLangOptions(options);
|
|
66
|
-
|
|
67
|
-
if (!langOptions) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
onAppMounted(() => Lang.setup());
|
|
72
|
-
|
|
73
|
-
const plugin = await createAppI18n(langOptions);
|
|
13
|
+
export default definePlugin({
|
|
14
|
+
async install(app) {
|
|
15
|
+
app.config.globalProperties.$t ??= translate;
|
|
16
|
+
app.config.globalProperties.$td = translateWithDefault;
|
|
74
17
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
await bootServices(app, services);
|
|
18
|
+
await bootServices(app, services);
|
|
19
|
+
},
|
|
78
20
|
});
|
|
79
21
|
|
|
80
|
-
declare module '@/
|
|
81
|
-
interface
|
|
82
|
-
lang?: LangOptions;
|
|
83
|
-
langMessages?: Record<string, unknown>;
|
|
84
|
-
}
|
|
22
|
+
declare module '@/services' {
|
|
23
|
+
export interface Services extends LangServices {}
|
|
85
24
|
}
|
|
86
25
|
|
|
87
|
-
declare module '
|
|
88
|
-
interface
|
|
26
|
+
declare module '@vue/runtime-core' {
|
|
27
|
+
interface ComponentCustomProperties {
|
|
28
|
+
$td: typeof translateWithDefault;
|
|
29
|
+
}
|
|
89
30
|
}
|
package/src/main.ts
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { GetClosureArgs } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
import App from '@/services/App';
|
|
4
|
+
|
|
5
|
+
import type { Plugin } from './Plugin';
|
|
6
|
+
|
|
7
|
+
export * from './Plugin';
|
|
8
|
+
|
|
9
|
+
export function definePlugin<T extends Plugin>(plugin: T): T {
|
|
10
|
+
return plugin;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function installPlugins(plugins: Plugin[], ...args: GetClosureArgs<Plugin['install']>): Promise<void> {
|
|
14
|
+
App.setState(
|
|
15
|
+
'plugins',
|
|
16
|
+
plugins.reduce((pluginsMap, plugin) => {
|
|
17
|
+
if (plugin.name) {
|
|
18
|
+
pluginsMap[plugin.name] = plugin;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return pluginsMap;
|
|
22
|
+
}, {} as Record<string, Plugin>),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
await Promise.all(plugins.map((plugin) => plugin.install(...args)) ?? []);
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Build from 'virtual:aerogel';
|
|
2
|
+
|
|
3
|
+
import { defineServiceState } from '@/services/Service';
|
|
4
|
+
import type { Plugin } from '@/plugins/Plugin';
|
|
5
|
+
|
|
6
|
+
export default defineServiceState({
|
|
7
|
+
name: 'app',
|
|
8
|
+
initialState: {
|
|
9
|
+
plugins: {} as Record<string, Plugin>,
|
|
10
|
+
environment: Build.environment,
|
|
11
|
+
sourceUrl: Build.sourceUrl,
|
|
12
|
+
isMounted: false,
|
|
13
|
+
},
|
|
14
|
+
computed: {
|
|
15
|
+
development: (state) => state.environment === 'development',
|
|
16
|
+
testing: (state) => state.environment === 'testing',
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { facade } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
import Events from '@/services/Events';
|
|
4
|
+
import type { Plugin } from '@/plugins';
|
|
5
|
+
|
|
6
|
+
import Service from './App.state';
|
|
7
|
+
|
|
8
|
+
export class AppService extends Service {
|
|
9
|
+
|
|
10
|
+
public plugin<T extends Plugin = Plugin>(name: string): T | null {
|
|
11
|
+
return (this.plugins[name] as T) ?? null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
protected async boot(): Promise<void> {
|
|
15
|
+
Events.once('application-mounted', () => this.setState({ isMounted: true }));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default facade(new AppService());
|