@aerogel/core 0.0.0-next.f1f5a990033d966dc0bb12d251110fbc9350dcc7 → 0.0.0-next.fd1bd21aea7a9ab8c4eab69a5f5776db5de8bf35
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 +511 -119
- package/dist/aerogel-core.esm.js +1 -1
- package/dist/aerogel-core.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/bootstrap/index.ts +6 -2
- package/src/bootstrap/options.ts +3 -0
- package/src/components/forms/AGCheckbox.vue +7 -1
- package/src/components/forms/AGForm.vue +9 -10
- package/src/components/forms/AGInput.vue +10 -6
- package/src/components/forms/AGSelect.story.vue +21 -3
- package/src/components/forms/AGSelect.vue +10 -3
- package/src/components/headless/forms/AGHeadlessButton.vue +17 -12
- package/src/components/headless/forms/AGHeadlessInput.ts +7 -3
- package/src/components/headless/forms/AGHeadlessInput.vue +3 -3
- package/src/components/headless/forms/AGHeadlessInputDescription.vue +28 -0
- package/src/components/headless/forms/AGHeadlessInputInput.vue +40 -4
- package/src/components/headless/forms/AGHeadlessInputTextArea.vue +40 -0
- package/src/components/headless/forms/AGHeadlessSelect.ts +15 -12
- package/src/components/headless/forms/AGHeadlessSelect.vue +23 -22
- package/src/components/headless/forms/AGHeadlessSelectOption.vue +6 -6
- package/src/components/headless/forms/composition.ts +10 -0
- package/src/components/headless/forms/index.ts +3 -0
- package/src/components/lib/AGErrorMessage.vue +2 -2
- package/src/components/lib/AGMarkdown.vue +7 -2
- package/src/components/modals/AGPromptModal.ts +30 -0
- package/src/components/modals/AGPromptModal.vue +34 -0
- package/src/components/modals/index.ts +10 -19
- package/src/directives/index.ts +2 -0
- package/src/directives/measure.ts +11 -2
- package/src/errors/Errors.ts +16 -19
- package/src/errors/index.ts +1 -10
- package/src/errors/utils.ts +19 -0
- package/src/forms/Form.ts +43 -3
- package/src/forms/index.ts +1 -0
- package/src/forms/utils.ts +15 -0
- package/src/jobs/Job.ts +5 -0
- package/src/jobs/index.ts +7 -0
- package/src/lang/Lang.ts +11 -23
- package/src/main.ts +3 -0
- package/src/services/App.state.ts +1 -2
- package/src/services/App.ts +21 -3
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.test.ts +39 -0
- package/src/services/Events.ts +100 -30
- package/src/services/Service.ts +42 -12
- package/src/services/index.ts +4 -1
- package/src/services/store.ts +8 -5
- package/src/testing/index.ts +25 -0
- package/src/ui/UI.ts +51 -6
- package/src/ui/index.ts +2 -0
|
@@ -9,31 +9,31 @@
|
|
|
9
9
|
[unselectedClass ?? 'unselected']: !selected,
|
|
10
10
|
}"
|
|
11
11
|
>
|
|
12
|
-
{{
|
|
12
|
+
{{ select.renderText(value) }}
|
|
13
13
|
</li>
|
|
14
14
|
</slot>
|
|
15
15
|
</ListboxOption>
|
|
16
16
|
</template>
|
|
17
17
|
|
|
18
18
|
<script setup lang="ts">
|
|
19
|
-
import { computed } from 'vue';
|
|
20
19
|
import { ListboxOption } from '@headlessui/vue';
|
|
21
20
|
|
|
22
21
|
import { injectReactiveOrFail, requiredMixedProp, stringProp } from '@/utils/vue';
|
|
23
22
|
import type { ComponentProps } from '@/utils/vue';
|
|
23
|
+
import type { FormFieldValue } from '@/forms/Form';
|
|
24
24
|
|
|
25
|
-
import type { IAGHeadlessSelect
|
|
25
|
+
import type { IAGHeadlessSelect } from './AGHeadlessSelect';
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
value: requiredMixedProp<
|
|
27
|
+
defineProps({
|
|
28
|
+
value: requiredMixedProp<FormFieldValue>(),
|
|
29
29
|
selectedClass: stringProp(),
|
|
30
30
|
unselectedClass: stringProp(),
|
|
31
31
|
activeClass: stringProp(),
|
|
32
32
|
inactiveClass: stringProp(),
|
|
33
33
|
});
|
|
34
|
+
|
|
34
35
|
const select = injectReactiveOrFail<IAGHeadlessSelect>(
|
|
35
36
|
'select',
|
|
36
37
|
'<AGHeadlessSelectOption> must be a child of a <AGHeadlessSelect>',
|
|
37
38
|
);
|
|
38
|
-
const option = computed(() => select.options.find((selectOption) => selectOption.value === props.value));
|
|
39
39
|
</script>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { inject, onUnmounted } from 'vue';
|
|
2
|
+
|
|
3
|
+
import type Form from '@/forms/Form';
|
|
4
|
+
|
|
5
|
+
export function onFormFocus(input: { name: string | null }, listener: () => unknown): void {
|
|
6
|
+
const form = inject<Form | null>('form', null);
|
|
7
|
+
const stop = form?.on('focus', (name) => input.name === name && listener());
|
|
8
|
+
|
|
9
|
+
onUnmounted(() => stop?.());
|
|
10
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
export * from './composition';
|
|
1
2
|
export * from './AGHeadlessInput';
|
|
2
3
|
export * from './AGHeadlessSelect';
|
|
3
4
|
export * from './AGHeadlessSelectOption';
|
|
4
5
|
export { default as AGHeadlessButton } from './AGHeadlessButton.vue';
|
|
5
6
|
export { default as AGHeadlessInput } from './AGHeadlessInput.vue';
|
|
7
|
+
export { default as AGHeadlessInputDescription } from './AGHeadlessInputDescription.vue';
|
|
6
8
|
export { default as AGHeadlessInputError } from './AGHeadlessInputError.vue';
|
|
7
9
|
export { default as AGHeadlessInputInput } from './AGHeadlessInputInput.vue';
|
|
8
10
|
export { default as AGHeadlessInputLabel } from './AGHeadlessInputLabel.vue';
|
|
11
|
+
export { default as AGHeadlessInputTextArea } from './AGHeadlessInputTextArea.vue';
|
|
9
12
|
export { default as AGHeadlessSelect } from './AGHeadlessSelect.vue';
|
|
10
13
|
export { default as AGHeadlessSelectButton } from './AGHeadlessSelectButton.vue';
|
|
11
14
|
export { default as AGHeadlessSelectError } from './AGHeadlessSelectError.vue';
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
<script setup lang="ts">
|
|
6
6
|
import { computed } from 'vue';
|
|
7
7
|
|
|
8
|
-
import Errors from '@/errors/Errors';
|
|
9
8
|
import { requiredObjectProp } from '@/utils/vue';
|
|
9
|
+
import { getErrorMessage } from '@/errors/utils';
|
|
10
10
|
import type { ErrorSource } from '@/errors/Errors.state';
|
|
11
11
|
|
|
12
12
|
import AGMarkdown from './AGMarkdown.vue';
|
|
13
13
|
|
|
14
14
|
const props = defineProps({ error: requiredObjectProp<ErrorSource>() });
|
|
15
|
-
const message = computed(() =>
|
|
15
|
+
const message = computed(() => getErrorMessage(props.error));
|
|
16
16
|
</script>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</template>
|
|
4
4
|
|
|
5
5
|
<script setup lang="ts">
|
|
6
|
-
import { computed, h } from 'vue';
|
|
6
|
+
import { computed, h, useAttrs } from 'vue';
|
|
7
7
|
|
|
8
8
|
import { renderMarkdown } from '@/utils/markdown';
|
|
9
9
|
import { booleanProp, objectProp, stringProp } from '@/utils/vue';
|
|
@@ -17,6 +17,7 @@ const props = defineProps({
|
|
|
17
17
|
text: stringProp(),
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
const attrs = useAttrs();
|
|
20
21
|
const markdown = computed(() => props.text ?? (props.langKey && translate(props.langKey, props.langParams ?? {})));
|
|
21
22
|
const html = computed(() => {
|
|
22
23
|
if (!markdown.value) {
|
|
@@ -32,5 +33,9 @@ const html = computed(() => {
|
|
|
32
33
|
return renderedHtml;
|
|
33
34
|
});
|
|
34
35
|
const root = () =>
|
|
35
|
-
h(props.as ?? (props.inline ? 'span' : 'div'), {
|
|
36
|
+
h(props.as ?? (props.inline ? 'span' : 'div'), {
|
|
37
|
+
innerHTML: html.value,
|
|
38
|
+
...attrs,
|
|
39
|
+
class: `${attrs.class ?? ''} ${props.inline ? '' : 'prose'}`,
|
|
40
|
+
});
|
|
36
41
|
</script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { computed } from 'vue';
|
|
2
|
+
import type { ExtractPropTypes } from 'vue';
|
|
3
|
+
import type { ObjectWithoutEmpty } from '@noeldemartin/utils';
|
|
4
|
+
|
|
5
|
+
import { requiredStringProp, stringProp } from '@/utils';
|
|
6
|
+
import { translateWithDefault } from '@/lang';
|
|
7
|
+
|
|
8
|
+
export const promptModalProps = {
|
|
9
|
+
title: stringProp(),
|
|
10
|
+
message: requiredStringProp(),
|
|
11
|
+
label: stringProp(),
|
|
12
|
+
defaultValue: stringProp(),
|
|
13
|
+
placeholder: stringProp(),
|
|
14
|
+
acceptText: stringProp(),
|
|
15
|
+
cancelText: stringProp(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type AGPromptModalProps = ObjectWithoutEmpty<ExtractPropTypes<typeof promptModalProps>>;
|
|
19
|
+
|
|
20
|
+
export function usePromptModalProps(): typeof promptModalProps {
|
|
21
|
+
return promptModalProps;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
25
|
+
export function usePromptModal(props: ExtractPropTypes<typeof promptModalProps>) {
|
|
26
|
+
const renderedAcceptText = computed(() => props.acceptText ?? translateWithDefault('ui.accept', 'Ok'));
|
|
27
|
+
const renderedCancelText = computed(() => props.cancelText ?? translateWithDefault('ui.cancel', 'Cancel'));
|
|
28
|
+
|
|
29
|
+
return { renderedAcceptText, renderedCancelText };
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AGModal v-slot="{ close }: IAGModalDefaultSlotProps" :cancellable="false" :title="title">
|
|
3
|
+
<AGMarkdown :text="message" />
|
|
4
|
+
|
|
5
|
+
<AGForm :form="form" @submit="close(form.draft)">
|
|
6
|
+
<AGInput name="draft" :placeholder="placeholder" :label="label" />
|
|
7
|
+
|
|
8
|
+
<div class="mt-2 flex flex-row-reverse gap-2">
|
|
9
|
+
<AGButton submit>
|
|
10
|
+
{{ renderedAcceptText }}
|
|
11
|
+
</AGButton>
|
|
12
|
+
<AGButton color="secondary" @click="close()">
|
|
13
|
+
{{ renderedCancelText }}
|
|
14
|
+
</AGButton>
|
|
15
|
+
</div>
|
|
16
|
+
</AGForm>
|
|
17
|
+
</AGModal>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
import AGModal from './AGModal.vue';
|
|
22
|
+
import { usePromptModal, usePromptModalProps } from './AGPromptModal';
|
|
23
|
+
import type { IAGModalDefaultSlotProps } from './AGModal';
|
|
24
|
+
|
|
25
|
+
import AGButton from '../forms/AGButton.vue';
|
|
26
|
+
import AGForm from '../forms/AGForm.vue';
|
|
27
|
+
import AGInput from '../forms/AGInput.vue';
|
|
28
|
+
import AGMarkdown from '../lib/AGMarkdown.vue';
|
|
29
|
+
import { requiredStringInput, useForm } from '../../forms';
|
|
30
|
+
|
|
31
|
+
const props = defineProps(usePromptModalProps());
|
|
32
|
+
const form = useForm({ draft: requiredStringInput(props.defaultValue ?? '') });
|
|
33
|
+
const { renderedAcceptText, renderedCancelText } = usePromptModal(props);
|
|
34
|
+
</script>
|
|
@@ -1,26 +1,17 @@
|
|
|
1
|
-
import AGAlertModal from './AGAlertModal.vue';
|
|
2
|
-
import AGConfirmModal from './AGConfirmModal.vue';
|
|
3
|
-
import AGErrorReportModalButtons from './AGErrorReportModalButtons.vue';
|
|
4
|
-
import AGErrorReportModalTitle from './AGErrorReportModalTitle.vue';
|
|
5
|
-
import AGLoadingModal from './AGLoadingModal.vue';
|
|
6
|
-
import AGModal from './AGModal.vue';
|
|
7
|
-
import AGModalTitle from './AGModalTitle.vue';
|
|
8
|
-
import AGModalContext from './AGModalContext.vue';
|
|
9
|
-
|
|
10
1
|
export * from './AGAlertModal';
|
|
11
2
|
export * from './AGConfirmModal';
|
|
12
3
|
export * from './AGErrorReportModal';
|
|
13
4
|
export * from './AGLoadingModal';
|
|
14
5
|
export * from './AGModal';
|
|
15
6
|
export * from './AGModalContext';
|
|
7
|
+
export * from './AGPromptModal';
|
|
16
8
|
|
|
17
|
-
export {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
};
|
|
9
|
+
export { default as AGAlertModal } from './AGAlertModal.vue';
|
|
10
|
+
export { default as AGConfirmModal } from './AGConfirmModal.vue';
|
|
11
|
+
export { default as AGErrorReportModalButtons } from './AGErrorReportModalButtons.vue';
|
|
12
|
+
export { default as AGErrorReportModalTitle } from './AGErrorReportModalTitle.vue';
|
|
13
|
+
export { default as AGLoadingModal } from './AGLoadingModal.vue';
|
|
14
|
+
export { default as AGModal } from './AGModal.vue';
|
|
15
|
+
export { default as AGModalContext } from './AGModalContext.vue';
|
|
16
|
+
export { default as AGModalTitle } from './AGModalTitle.vue';
|
|
17
|
+
export { default as AGPromptModal } from './AGPromptModal.vue';
|
package/src/directives/index.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { defineDirective } from '@/utils/vue';
|
|
2
2
|
|
|
3
|
+
export interface ElementSize {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type MeasureDirectiveListener = (size: ElementSize) => unknown;
|
|
9
|
+
|
|
3
10
|
export default defineDirective({
|
|
4
|
-
mounted(element: HTMLElement, { value }
|
|
11
|
+
mounted(element: HTMLElement, { value }) {
|
|
12
|
+
const listener = typeof value === 'function' ? (value as MeasureDirectiveListener) : null;
|
|
5
13
|
const sizes = element.getBoundingClientRect();
|
|
6
14
|
|
|
15
|
+
// TODO guard with modifiers.css once typed properly
|
|
7
16
|
element.style.setProperty('--width', `${sizes.width}px`);
|
|
8
17
|
element.style.setProperty('--height', `${sizes.height}px`);
|
|
9
18
|
|
|
10
|
-
|
|
19
|
+
listener?.({ width: sizes.width, height: sizes.height });
|
|
11
20
|
},
|
|
12
21
|
});
|
package/src/errors/Errors.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { translateWithDefault } from '@/lang/utils';
|
|
|
7
7
|
|
|
8
8
|
import Service from './Errors.state';
|
|
9
9
|
import { Colors } from '@/components/constants';
|
|
10
|
+
import { Events } from '@/services';
|
|
10
11
|
import type { AGErrorReportModalProps } from '@/components/modals/AGErrorReportModal';
|
|
11
12
|
import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
12
13
|
import type { ModalComponent } from '@/ui/UI.state';
|
|
@@ -39,7 +40,13 @@ export class ErrorsService extends Service {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
public async report(error: ErrorSource, message?: string): Promise<void> {
|
|
42
|
-
|
|
43
|
+
await Events.emit('error', { error, message });
|
|
44
|
+
|
|
45
|
+
if (App.testing) {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (App.development) {
|
|
43
50
|
this.logError(error);
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -47,7 +54,7 @@ export class ErrorsService extends Service {
|
|
|
47
54
|
throw error;
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
if (!App.isMounted) {
|
|
57
|
+
if (!App.isMounted()) {
|
|
51
58
|
const startupError = await this.createStartupErrorReport(error);
|
|
52
59
|
|
|
53
60
|
if (startupError) {
|
|
@@ -110,22 +117,6 @@ export class ErrorsService extends Service {
|
|
|
110
117
|
});
|
|
111
118
|
}
|
|
112
119
|
|
|
113
|
-
public getErrorMessage(error: ErrorSource): string {
|
|
114
|
-
if (typeof error === 'string') {
|
|
115
|
-
return error;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (error instanceof Error || error instanceof JSError) {
|
|
119
|
-
return error.message;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (isObject(error)) {
|
|
123
|
-
return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return translateWithDefault('errors.unknown', 'Unknown Error');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
120
|
private logError(error: unknown): void {
|
|
130
121
|
// eslint-disable-next-line no-console
|
|
131
122
|
console.error(error);
|
|
@@ -185,4 +176,10 @@ export class ErrorsService extends Service {
|
|
|
185
176
|
|
|
186
177
|
}
|
|
187
178
|
|
|
188
|
-
export default facade(
|
|
179
|
+
export default facade(ErrorsService);
|
|
180
|
+
|
|
181
|
+
declare module '@/services/Events' {
|
|
182
|
+
export interface EventsPayload {
|
|
183
|
+
error: { error: ErrorSource; message?: string };
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/errors/index.ts
CHANGED
|
@@ -6,20 +6,11 @@ import { definePlugin } from '@/plugins';
|
|
|
6
6
|
import Errors from './Errors';
|
|
7
7
|
import { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
8
8
|
|
|
9
|
+
export * from './utils';
|
|
9
10
|
export { Errors, ErrorSource, ErrorReport, ErrorReportLog };
|
|
10
11
|
|
|
11
12
|
const services = { $errors: Errors };
|
|
12
13
|
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
14
|
Errors.report(error);
|
|
24
15
|
|
|
25
16
|
return true;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { JSError, isObject, toString } from '@noeldemartin/utils';
|
|
2
|
+
import { translateWithDefault } from '@/lang/utils';
|
|
3
|
+
import type { ErrorSource } from './Errors.state';
|
|
4
|
+
|
|
5
|
+
export function getErrorMessage(error: ErrorSource): string {
|
|
6
|
+
if (typeof error === 'string') {
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (error instanceof Error || error instanceof JSError) {
|
|
11
|
+
return error.message;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (isObject(error)) {
|
|
15
|
+
return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return translateWithDefault('errors.unknown', 'Unknown Error');
|
|
19
|
+
}
|
package/src/forms/Form.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MagicObject } from '@noeldemartin/utils';
|
|
2
|
-
import { computed, reactive, readonly, ref } from 'vue';
|
|
1
|
+
import { MagicObject, arrayRemove } from '@noeldemartin/utils';
|
|
2
|
+
import { computed, nextTick, reactive, readonly, ref } from 'vue';
|
|
3
3
|
import type { ObjectValues } from '@noeldemartin/utils';
|
|
4
4
|
import type { ComputedRef, DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
|
|
5
5
|
|
|
@@ -8,6 +8,7 @@ export const FormFieldTypes = {
|
|
|
8
8
|
Number: 'number',
|
|
9
9
|
Boolean: 'boolean',
|
|
10
10
|
Object: 'object',
|
|
11
|
+
Date: 'date',
|
|
11
12
|
} as const;
|
|
12
13
|
|
|
13
14
|
export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
|
|
@@ -18,6 +19,7 @@ export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType
|
|
|
18
19
|
|
|
19
20
|
export type FormFieldDefinitions = Record<string, FormFieldDefinition>;
|
|
20
21
|
export type FormFieldType = ObjectValues<typeof FormFieldTypes>;
|
|
22
|
+
export type FormFieldValue = GetFormFieldValue<FormFieldType>;
|
|
21
23
|
|
|
22
24
|
export type FormData<T> = {
|
|
23
25
|
-readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
|
|
@@ -39,10 +41,15 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
|
|
|
39
41
|
? boolean
|
|
40
42
|
: TType extends typeof FormFieldTypes.Object
|
|
41
43
|
? object
|
|
44
|
+
: TType extends typeof FormFieldTypes.Date
|
|
45
|
+
? Date
|
|
42
46
|
: never;
|
|
43
47
|
|
|
44
48
|
const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
|
|
45
49
|
|
|
50
|
+
export type SubmitFormListener = () => unknown;
|
|
51
|
+
export type FocusFormListener = (input: string) => unknown;
|
|
52
|
+
|
|
46
53
|
export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
|
|
47
54
|
|
|
48
55
|
public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
|
|
@@ -51,6 +58,7 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
51
58
|
private _data: FormData<Fields>;
|
|
52
59
|
private _submitted: Ref<boolean>;
|
|
53
60
|
private _errors: FormErrors<Fields>;
|
|
61
|
+
private _listeners: { focus?: FocusFormListener[]; submit?: SubmitFormListener[] } = {};
|
|
54
62
|
|
|
55
63
|
constructor(fields: Fields) {
|
|
56
64
|
super();
|
|
@@ -88,6 +96,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
88
96
|
return this._data[field] as unknown as GetFormFieldValue<Fields[T]['type']>;
|
|
89
97
|
}
|
|
90
98
|
|
|
99
|
+
public data(): FormData<Fields> {
|
|
100
|
+
return { ...this._data };
|
|
101
|
+
}
|
|
102
|
+
|
|
91
103
|
public validate(): boolean {
|
|
92
104
|
const errors = Object.entries(this._fields).reduce((formErrors, [name, definition]) => {
|
|
93
105
|
formErrors[name] = this.getFieldErrors(name, definition);
|
|
@@ -110,7 +122,35 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
110
122
|
public submit(): boolean {
|
|
111
123
|
this._submitted.value = true;
|
|
112
124
|
|
|
113
|
-
|
|
125
|
+
const valid = this.validate();
|
|
126
|
+
|
|
127
|
+
valid && this._listeners['submit']?.forEach((listener) => listener());
|
|
128
|
+
|
|
129
|
+
return valid;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public on(event: 'focus', listener: FocusFormListener): () => void;
|
|
133
|
+
public on(event: 'submit', listener: SubmitFormListener): () => void;
|
|
134
|
+
public on(event: 'focus' | 'submit', listener: FocusFormListener | SubmitFormListener): () => void {
|
|
135
|
+
this._listeners[event] ??= [];
|
|
136
|
+
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
138
|
+
this._listeners[event]?.push(listener as any);
|
|
139
|
+
|
|
140
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
141
|
+
return () => this.off(event as any, listener);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public off(event: 'focus', listener: FocusFormListener): void;
|
|
145
|
+
public off(event: 'submit', listener: SubmitFormListener): void;
|
|
146
|
+
public off(event: 'focus' | 'submit', listener: FocusFormListener | SubmitFormListener): void {
|
|
147
|
+
arrayRemove(this._listeners[event] ?? [], listener);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public async focus(input: string): Promise<void> {
|
|
151
|
+
await nextTick();
|
|
152
|
+
|
|
153
|
+
this._listeners['focus']?.forEach((listener) => listener(input));
|
|
114
154
|
}
|
|
115
155
|
|
|
116
156
|
protected __get(property: string): unknown {
|
package/src/forms/index.ts
CHANGED
package/src/forms/utils.ts
CHANGED
|
@@ -8,6 +8,13 @@ export function booleanInput(defaultValue?: boolean): FormFieldDefinition<typeof
|
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export function dateInput(defaultValue?: Date): FormFieldDefinition<typeof FormFieldTypes.Date> {
|
|
12
|
+
return {
|
|
13
|
+
default: defaultValue,
|
|
14
|
+
type: FormFieldTypes.Date,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
export function requiredBooleanInput(
|
|
12
19
|
defaultValue?: boolean,
|
|
13
20
|
): FormFieldDefinition<typeof FormFieldTypes.Boolean, 'required'> {
|
|
@@ -18,6 +25,14 @@ export function requiredBooleanInput(
|
|
|
18
25
|
};
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
export function requiredDateInput(defaultValue?: Date): FormFieldDefinition<typeof FormFieldTypes.Date> {
|
|
29
|
+
return {
|
|
30
|
+
default: defaultValue,
|
|
31
|
+
type: FormFieldTypes.Date,
|
|
32
|
+
rules: 'required',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
export function requiredNumberInput(
|
|
22
37
|
defaultValue?: number,
|
|
23
38
|
): FormFieldDefinition<typeof FormFieldTypes.Number, 'required'> {
|
package/src/jobs/Job.ts
ADDED
package/src/lang/Lang.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { facade
|
|
1
|
+
import { facade } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
3
|
import App from '@/services/App';
|
|
4
4
|
import Service from '@/services/Service';
|
|
5
5
|
|
|
6
6
|
export interface LangProvider {
|
|
7
7
|
translate(key: string, parameters?: Record<string, unknown>): string;
|
|
8
|
+
translateWithDefault(key: string, defaultMessage: string, parameters?: Record<string, unknown>): string;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export class LangService extends Service {
|
|
@@ -21,6 +22,12 @@ export class LangService extends Service {
|
|
|
21
22
|
|
|
22
23
|
return key;
|
|
23
24
|
},
|
|
25
|
+
translateWithDefault: (_, defaultMessage) => {
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
App.development && console.warn('Lang provider is missing');
|
|
28
|
+
|
|
29
|
+
return defaultMessage;
|
|
30
|
+
},
|
|
24
31
|
};
|
|
25
32
|
}
|
|
26
33
|
|
|
@@ -32,29 +39,10 @@ export class LangService extends Service {
|
|
|
32
39
|
return this.provider.translate(key, parameters) ?? key;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
public translateWithDefault(key: string, defaultMessage: string): string
|
|
36
|
-
|
|
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;
|
|
42
|
+
public translateWithDefault(key: string, defaultMessage: string, parameters: Record<string, unknown> = {}): string {
|
|
43
|
+
return this.provider.translateWithDefault(key, defaultMessage, parameters);
|
|
56
44
|
}
|
|
57
45
|
|
|
58
46
|
}
|
|
59
47
|
|
|
60
|
-
export default facade(
|
|
48
|
+
export default facade(LangService);
|
package/src/main.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export * from './bootstrap';
|
|
2
2
|
export * from './components';
|
|
3
|
+
export * from './directives';
|
|
3
4
|
export * from './errors';
|
|
4
5
|
export * from './forms';
|
|
6
|
+
export * from './jobs';
|
|
5
7
|
export * from './lang';
|
|
6
8
|
export * from './plugins';
|
|
7
9
|
export * from './services';
|
|
10
|
+
export * from './testing';
|
|
8
11
|
export * from './ui';
|
|
9
12
|
export * from './utils';
|
|
@@ -9,10 +9,9 @@ export default defineServiceState({
|
|
|
9
9
|
plugins: {} as Record<string, Plugin>,
|
|
10
10
|
environment: Aerogel.environment,
|
|
11
11
|
sourceUrl: Aerogel.sourceUrl,
|
|
12
|
-
isMounted: false,
|
|
13
12
|
},
|
|
14
13
|
computed: {
|
|
15
14
|
development: (state) => state.environment === 'development',
|
|
16
|
-
testing: (state) => state.environment === 'testing',
|
|
15
|
+
testing: (state) => state.environment === 'test' || state.environment === 'testing',
|
|
17
16
|
},
|
|
18
17
|
});
|
package/src/services/App.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { facade, forever, updateLocationQueryParameters } from '@noeldemartin/utils';
|
|
1
|
+
import { PromisedValue, facade, forever, updateLocationQueryParameters } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
3
|
import Events from '@/services/Events';
|
|
4
4
|
import type { Plugin } from '@/plugins';
|
|
@@ -7,6 +7,23 @@ import Service from './App.state';
|
|
|
7
7
|
|
|
8
8
|
export class AppService extends Service {
|
|
9
9
|
|
|
10
|
+
public readonly ready = new PromisedValue<void>();
|
|
11
|
+
public readonly mounted = new PromisedValue<void>();
|
|
12
|
+
|
|
13
|
+
public isReady(): boolean {
|
|
14
|
+
return this.ready.isResolved();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public isMounted(): boolean {
|
|
18
|
+
return this.mounted.isResolved();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async whenReady<T>(callback: () => T): Promise<T> {
|
|
22
|
+
const result = await this.ready.then(callback);
|
|
23
|
+
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
public async reload(queryParameters?: Record<string, string | undefined>): Promise<void> {
|
|
11
28
|
queryParameters && updateLocationQueryParameters(queryParameters);
|
|
12
29
|
|
|
@@ -21,9 +38,10 @@ export class AppService extends Service {
|
|
|
21
38
|
}
|
|
22
39
|
|
|
23
40
|
protected async boot(): Promise<void> {
|
|
24
|
-
Events.once('application-
|
|
41
|
+
Events.once('application-ready', () => this.ready.resolve());
|
|
42
|
+
Events.once('application-mounted', () => this.mounted.resolve());
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
}
|
|
28
46
|
|
|
29
|
-
export default facade(
|
|
47
|
+
export default facade(AppService);
|