@aerogel/core 0.0.0-next.bde642c4a8096c5fc3d5e676c2115da23f4bf1d8 → 0.0.0-next.c2e6acc000e97a1020c2e232678563c53884dd0e
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.d.ts +1396 -1645
- package/dist/aerogel-core.js +2968 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +27 -37
- package/src/bootstrap/bootstrap.test.ts +4 -7
- package/src/bootstrap/index.ts +25 -16
- package/src/bootstrap/options.ts +1 -1
- package/src/components/{AGAppLayout.vue → AppLayout.vue} +4 -4
- package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
- package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +5 -5
- package/src/components/composition.ts +1 -1
- package/src/components/contracts/AlertModal.ts +4 -0
- package/src/components/contracts/Button.ts +15 -0
- package/src/components/contracts/ConfirmModal.ts +41 -0
- package/src/components/contracts/ErrorReportModal.ts +29 -0
- package/src/components/contracts/Input.ts +26 -0
- package/src/components/contracts/LoadingModal.ts +18 -0
- package/src/components/contracts/Modal.ts +9 -0
- package/src/components/contracts/PromptModal.ts +28 -0
- package/src/components/contracts/index.ts +7 -0
- package/src/components/contracts/shared.ts +9 -0
- package/src/components/forms/AGSelect.vue +11 -17
- package/src/components/forms/index.ts +0 -4
- package/src/components/headless/HeadlessButton.vue +45 -0
- package/src/components/headless/HeadlessInput.vue +59 -0
- package/src/components/headless/{forms/AGHeadlessInputDescription.vue → HeadlessInputDescription.vue} +6 -7
- package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
- package/src/components/headless/{forms/AGHeadlessInputInput.vue → HeadlessInputInput.vue} +11 -19
- package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
- package/src/components/headless/{forms/AGHeadlessInputTextArea.vue → HeadlessInputTextArea.vue} +7 -9
- package/src/components/headless/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +16 -18
- package/src/components/headless/HeadlessModalContent.vue +24 -0
- package/src/components/headless/HeadlessModalOverlay.vue +12 -0
- package/src/components/headless/HeadlessModalTitle.vue +12 -0
- package/src/components/headless/forms/AGHeadlessSelect.ts +3 -3
- package/src/components/headless/forms/AGHeadlessSelect.vue +16 -16
- package/src/components/headless/forms/AGHeadlessSelectError.vue +2 -2
- package/src/components/headless/forms/AGHeadlessSelectOption.vue +10 -18
- package/src/components/headless/forms/AGHeadlessSelectOptions.vue +19 -0
- package/src/components/headless/forms/AGHeadlessSelectTrigger.vue +25 -0
- package/src/components/headless/forms/composition.ts +2 -2
- package/src/components/headless/forms/index.ts +2 -12
- package/src/components/headless/index.ts +12 -1
- package/src/components/headless/snackbars/index.ts +3 -3
- package/src/components/index.ts +5 -5
- package/src/components/lib/AGErrorMessage.vue +5 -5
- package/src/components/lib/AGMeasured.vue +3 -2
- package/src/components/lib/AGStartupCrash.vue +8 -8
- package/src/components/lib/index.ts +0 -2
- package/src/components/snackbars/AGSnackbar.vue +10 -8
- package/src/components/ui/AlertModal.vue +13 -0
- package/src/components/ui/Button.vue +58 -0
- package/src/components/ui/Checkbox.vue +49 -0
- package/src/components/ui/ConfirmModal.vue +42 -0
- package/src/components/ui/ErrorReportModal.vue +62 -0
- package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +29 -20
- package/src/components/ui/ErrorReportModalTitle.vue +24 -0
- package/src/components/{forms/AGForm.vue → ui/Form.vue} +4 -5
- package/src/components/ui/Input.vue +52 -0
- package/src/components/ui/Link.vue +12 -0
- package/src/components/ui/LoadingModal.vue +32 -0
- package/src/components/ui/Markdown.vue +62 -0
- package/src/components/ui/Modal.vue +55 -0
- package/src/components/ui/ModalContext.vue +30 -0
- package/src/components/ui/ProgressBar.vue +50 -0
- package/src/components/ui/PromptModal.vue +35 -0
- package/src/components/ui/index.ts +16 -0
- package/src/components/utils.ts +106 -9
- package/src/directives/index.ts +9 -5
- package/src/directives/measure.ts +25 -6
- package/src/errors/Errors.state.ts +1 -1
- package/src/errors/Errors.ts +14 -14
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +9 -6
- package/src/errors/utils.ts +17 -1
- package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
- package/src/forms/{Form.ts → FormController.ts} +46 -25
- package/src/forms/composition.ts +4 -4
- package/src/forms/index.ts +3 -2
- package/src/forms/utils.ts +22 -6
- package/src/forms/validation.ts +19 -0
- package/src/index.css +8 -0
- package/src/jobs/Job.ts +144 -2
- package/src/jobs/index.ts +4 -1
- package/src/jobs/listeners.ts +3 -0
- package/src/jobs/status.ts +4 -0
- package/src/lang/DefaultLangProvider.ts +7 -4
- package/src/lang/Lang.state.ts +1 -1
- package/src/lang/Lang.ts +5 -1
- package/src/lang/index.ts +8 -6
- package/src/plugins/Plugin.ts +1 -1
- package/src/plugins/index.ts +10 -7
- package/src/services/App.state.ts +13 -4
- package/src/services/App.ts +8 -3
- package/src/services/Cache.ts +1 -1
- package/src/services/Events.ts +15 -5
- package/src/services/Service.ts +116 -53
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +11 -5
- package/src/services/utils.ts +18 -0
- package/src/testing/index.ts +4 -3
- package/src/testing/setup.ts +5 -13
- package/src/ui/UI.state.ts +17 -5
- package/src/ui/UI.ts +168 -62
- package/src/ui/index.ts +17 -16
- package/src/ui/utils.ts +16 -0
- package/src/utils/composition/events.ts +2 -2
- package/src/utils/composition/forms.ts +4 -3
- package/src/utils/composition/persistent.test.ts +33 -0
- package/src/utils/composition/persistent.ts +11 -0
- package/src/utils/composition/state.test.ts +47 -0
- package/src/utils/composition/state.ts +24 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/markdown.test.ts +50 -0
- package/src/utils/markdown.ts +19 -6
- package/src/utils/vdom.ts +31 -0
- package/src/utils/vue.ts +18 -13
- package/dist/aerogel-core.cjs.js +0 -2
- package/dist/aerogel-core.cjs.js.map +0 -1
- package/dist/aerogel-core.esm.js +0 -2
- package/dist/aerogel-core.esm.js.map +0 -1
- package/histoire.config.ts +0 -7
- package/noeldemartin.config.js +0 -5
- package/postcss.config.js +0 -6
- package/src/assets/histoire.css +0 -3
- package/src/components/forms/AGButton.vue +0 -44
- package/src/components/forms/AGCheckbox.vue +0 -41
- package/src/components/forms/AGInput.vue +0 -40
- package/src/components/headless/forms/AGHeadlessButton.ts +0 -3
- package/src/components/headless/forms/AGHeadlessButton.vue +0 -62
- package/src/components/headless/forms/AGHeadlessInput.ts +0 -33
- package/src/components/headless/forms/AGHeadlessInput.vue +0 -63
- package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
- package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
- package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
- package/src/components/headless/modals/index.ts +0 -4
- package/src/components/interfaces.ts +0 -24
- package/src/components/lib/AGLink.vue +0 -9
- package/src/components/lib/AGMarkdown.vue +0 -41
- package/src/components/modals/AGAlertModal.ts +0 -15
- package/src/components/modals/AGAlertModal.vue +0 -14
- package/src/components/modals/AGConfirmModal.ts +0 -33
- package/src/components/modals/AGConfirmModal.vue +0 -26
- package/src/components/modals/AGErrorReportModal.ts +0 -46
- package/src/components/modals/AGErrorReportModal.vue +0 -54
- package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
- package/src/components/modals/AGLoadingModal.ts +0 -23
- package/src/components/modals/AGLoadingModal.vue +0 -15
- package/src/components/modals/AGModal.ts +0 -10
- package/src/components/modals/AGModal.vue +0 -39
- package/src/components/modals/AGModalContext.ts +0 -8
- package/src/components/modals/AGModalContext.vue +0 -22
- package/src/components/modals/AGModalTitle.vue +0 -9
- package/src/components/modals/AGPromptModal.ts +0 -36
- package/src/components/modals/AGPromptModal.vue +0 -34
- package/src/components/modals/index.ts +0 -17
- package/src/directives/initial-focus.ts +0 -11
- package/src/main.histoire.ts +0 -1
- package/tailwind.config.js +0 -4
- package/tsconfig.json +0 -11
- package/vite.config.ts +0 -17
- /package/src/components/{AGAppSnackbars.vue → AppSnackbars.vue} +0 -0
- /package/src/{main.ts → index.ts} +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-1 h-2 w-full overflow-hidden rounded-full bg-gray-200">
|
|
3
|
+
<div :class="filledClasses" :style="`transform:translateX(-${(1 - renderedProgress) * 100}%)`" />
|
|
4
|
+
<span class="sr-only">
|
|
5
|
+
{{
|
|
6
|
+
$td('ui.progress', '{progress}% complete', {
|
|
7
|
+
progress: renderedProgress * 100,
|
|
8
|
+
})
|
|
9
|
+
}}
|
|
10
|
+
</span>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
import { computed, onUnmounted, ref, watch } from 'vue';
|
|
16
|
+
|
|
17
|
+
import { classes } from '@aerogel/core/components/utils';
|
|
18
|
+
import type Job from '@aerogel/core/jobs/Job';
|
|
19
|
+
|
|
20
|
+
const { filledClass, progress, job } = defineProps<{
|
|
21
|
+
filledClass?: string;
|
|
22
|
+
progress?: number;
|
|
23
|
+
job?: Job;
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
let cleanup: Function | undefined;
|
|
27
|
+
const jobProgress = ref(0);
|
|
28
|
+
const filledClasses = computed(() =>
|
|
29
|
+
classes('size-full transition-transform duration-500 rounded-r-full ease-linear bg-primary', filledClass));
|
|
30
|
+
const renderedProgress = computed(() => {
|
|
31
|
+
if (typeof progress === 'number') {
|
|
32
|
+
return progress;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return jobProgress.value;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
watch(
|
|
39
|
+
() => job,
|
|
40
|
+
() => {
|
|
41
|
+
cleanup?.();
|
|
42
|
+
|
|
43
|
+
jobProgress.value = job?.progress ?? 0;
|
|
44
|
+
cleanup = job?.listeners.add({ onUpdated: (value) => (jobProgress.value = value) });
|
|
45
|
+
},
|
|
46
|
+
{ immediate: true },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
onUnmounted(() => cleanup?.());
|
|
50
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Modal v-slot="{ close }" :title="title" persistent>
|
|
3
|
+
<Form :form="form" @submit="close(form.draft)">
|
|
4
|
+
<Markdown :text="message" />
|
|
5
|
+
<Input
|
|
6
|
+
name="draft"
|
|
7
|
+
class="mt-2"
|
|
8
|
+
:placeholder="placeholder"
|
|
9
|
+
:label="label"
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
<div class="mt-4 flex flex-row-reverse gap-2">
|
|
13
|
+
<Button :variant="acceptVariant" submit>
|
|
14
|
+
{{ renderedAcceptText }}
|
|
15
|
+
</Button>
|
|
16
|
+
<Button :variant="cancelVariant" @click="close(false)">
|
|
17
|
+
{{ renderedCancelText }}
|
|
18
|
+
</Button>
|
|
19
|
+
</div>
|
|
20
|
+
</Form>
|
|
21
|
+
</Modal>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import Markdown from '@aerogel/core/components/ui/Markdown.vue';
|
|
26
|
+
import Button from '@aerogel/core/components/ui/Button.vue';
|
|
27
|
+
import Form from '@aerogel/core/components/ui/Form.vue';
|
|
28
|
+
import Input from '@aerogel/core/components/ui/Input.vue';
|
|
29
|
+
import Modal from '@aerogel/core/components/ui/Modal.vue';
|
|
30
|
+
import { usePromptModal } from '@aerogel/core/components/contracts/PromptModal';
|
|
31
|
+
import type { PromptModalProps } from '@aerogel/core/components/contracts/PromptModal';
|
|
32
|
+
|
|
33
|
+
const { cancelVariant = 'secondary', ...props } = defineProps<PromptModalProps>();
|
|
34
|
+
const { form, renderedAcceptText, renderedCancelText } = usePromptModal(props);
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { default as AlertModal } from './AlertModal.vue';
|
|
2
|
+
export { default as Button } from './Button.vue';
|
|
3
|
+
export { default as Checkbox } from './Checkbox.vue';
|
|
4
|
+
export { default as ConfirmModal } from './ConfirmModal.vue';
|
|
5
|
+
export { default as ErrorReportModal } from './ErrorReportModal.vue';
|
|
6
|
+
export { default as ErrorReportModalButtons } from './ErrorReportModalButtons.vue';
|
|
7
|
+
export { default as ErrorReportModalTitle } from './ErrorReportModalTitle.vue';
|
|
8
|
+
export { default as Form } from './Form.vue';
|
|
9
|
+
export { default as Input } from './Input.vue';
|
|
10
|
+
export { default as Link } from './Link.vue';
|
|
11
|
+
export { default as LoadingModal } from './LoadingModal.vue';
|
|
12
|
+
export { default as Markdown } from './Markdown.vue';
|
|
13
|
+
export { default as Modal } from './Modal.vue';
|
|
14
|
+
export { default as ModalContext } from './ModalContext.vue';
|
|
15
|
+
export { default as ProgressBar } from './ProgressBar.vue';
|
|
16
|
+
export { default as PromptModal } from './PromptModal.vue';
|
package/src/components/utils.ts
CHANGED
|
@@ -1,10 +1,107 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { computed, customRef, inject, onUnmounted, unref } from 'vue';
|
|
3
|
+
import { cva } from 'class-variance-authority';
|
|
4
|
+
import { isObject } from '@noeldemartin/utils';
|
|
5
|
+
import { twMerge } from 'tailwind-merge';
|
|
6
|
+
import type { ClassValue } from 'clsx';
|
|
7
|
+
import type { ComputedRef, ExtractPropTypes, PropType, Ref, UnwrapNestedRefs } from 'vue';
|
|
8
|
+
import type { GetClosureArgs, GetClosureResult, Nullable } from '@noeldemartin/utils';
|
|
9
|
+
|
|
10
|
+
import type { HasElement } from '@aerogel/core/components/contracts/shared';
|
|
11
|
+
import type { FormController } from '@aerogel/core/forms';
|
|
12
|
+
|
|
13
|
+
export type CVAConfig<T> = NonNullable<GetClosureArgs<typeof cva<T>>[1]>;
|
|
14
|
+
export type CVAProps<T> = NonNullable<GetClosureArgs<GetClosureResult<typeof cva<T>>>[0]>;
|
|
15
|
+
export type RefsObject<T> = { [K in keyof T]: Ref<T[K]> | T[K] };
|
|
16
|
+
export type Variants<T extends Record<string, string>> = Required<{
|
|
17
|
+
[K in keyof T]: {
|
|
18
|
+
[key in T[K]]: string;
|
|
19
|
+
};
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
export type ComponentPropDefinitions<T> = {
|
|
23
|
+
[K in keyof T]: {
|
|
24
|
+
type?: PropType<T[K]>;
|
|
25
|
+
default: T[K] | (() => T[K]) | null;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type PickComponentProps<TValues, TDefinitions> = {
|
|
30
|
+
[K in keyof TValues]: K extends keyof TDefinitions ? TValues[K] : never;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function computedVariantClasses<T>(
|
|
34
|
+
value: RefsObject<{ baseClasses?: string } & CVAProps<T>>,
|
|
35
|
+
config: { baseClasses?: string } & CVAConfig<T>,
|
|
36
|
+
): ComputedRef<string> {
|
|
37
|
+
return computed(() => {
|
|
38
|
+
const { baseClasses: valueBaseClasses, ...valueRefs } = value;
|
|
39
|
+
const { baseClasses: configBaseClasses, ...configs } = config;
|
|
40
|
+
const variants = cva(configBaseClasses, configs as CVAConfig<T>);
|
|
41
|
+
const values = Object.entries(valueRefs).reduce((extractedValues, [name, valueRef]) => {
|
|
42
|
+
extractedValues[name as keyof CVAProps<T>] = unref(valueRef);
|
|
43
|
+
|
|
44
|
+
return extractedValues;
|
|
45
|
+
}, {} as CVAProps<T>);
|
|
46
|
+
|
|
47
|
+
return classes(variants(values), unref(valueBaseClasses));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function classes(...inputs: ClassValue[]): string {
|
|
52
|
+
return twMerge(clsx(inputs));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function elementRef(): Ref<HTMLElement | undefined> {
|
|
56
|
+
return customRef((track, trigger) => {
|
|
57
|
+
let value: HTMLElement | undefined = undefined;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
get() {
|
|
61
|
+
track();
|
|
62
|
+
|
|
63
|
+
return value;
|
|
64
|
+
},
|
|
65
|
+
set(newValue) {
|
|
66
|
+
value = getElement(newValue);
|
|
67
|
+
|
|
68
|
+
trigger();
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function extractComponentProps<TDefinitions extends {}, TValues extends ExtractPropTypes<TDefinitions>>(
|
|
75
|
+
values: TValues,
|
|
76
|
+
definitions: TDefinitions,
|
|
77
|
+
): PickComponentProps<TValues, TDefinitions> {
|
|
78
|
+
return Object.keys(definitions).reduce(
|
|
79
|
+
(extracted, prop) => {
|
|
80
|
+
extracted[prop] = values[prop as keyof TValues];
|
|
81
|
+
|
|
82
|
+
return extracted;
|
|
83
|
+
},
|
|
84
|
+
{} as Record<string, unknown>,
|
|
85
|
+
) as PickComponentProps<TValues, TDefinitions>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getElement(value: unknown): HTMLElement | undefined {
|
|
89
|
+
if (value instanceof HTMLElement) {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (hasElement(value)) {
|
|
94
|
+
return value.$el;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function hasElement(value: unknown): value is UnwrapNestedRefs<HasElement> {
|
|
99
|
+
return isObject(value) && '$el' in value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function onFormFocus(input: { name: Nullable<string> }, listener: () => unknown): void {
|
|
103
|
+
const form = inject<FormController | null>('form', null);
|
|
104
|
+
const stop = form?.on('focus', (name) => input.name === name && listener());
|
|
105
|
+
|
|
106
|
+
onUnmounted(() => stop?.());
|
|
10
107
|
}
|
package/src/directives/index.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { Directive } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { definePlugin } from '
|
|
3
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
4
4
|
|
|
5
|
-
import initialFocus from './initial-focus';
|
|
6
5
|
import measure from './measure';
|
|
7
6
|
|
|
8
7
|
const builtInDirectives: Record<string, Directive> = {
|
|
9
|
-
|
|
10
|
-
'measure': measure,
|
|
8
|
+
measure: measure,
|
|
11
9
|
};
|
|
12
10
|
|
|
13
11
|
export * from './measure';
|
|
@@ -25,8 +23,14 @@ export default definePlugin({
|
|
|
25
23
|
},
|
|
26
24
|
});
|
|
27
25
|
|
|
28
|
-
declare module '
|
|
26
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
29
27
|
export interface AerogelOptions {
|
|
30
28
|
directives?: Record<string, Directive>;
|
|
31
29
|
}
|
|
32
30
|
}
|
|
31
|
+
|
|
32
|
+
declare module 'vue' {
|
|
33
|
+
interface ComponentCustomDirectives {
|
|
34
|
+
measure: Directive<string, string>;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { defineDirective } from '
|
|
1
|
+
import { defineDirective } from '@aerogel/core/utils/vue';
|
|
2
|
+
import { tap } from '@noeldemartin/utils';
|
|
3
|
+
|
|
4
|
+
const resizeObservers: WeakMap<HTMLElement, ResizeObserver> = new WeakMap();
|
|
2
5
|
|
|
3
6
|
export interface ElementSize {
|
|
4
7
|
width: number;
|
|
@@ -9,13 +12,29 @@ export type MeasureDirectiveListener = (size: ElementSize) => unknown;
|
|
|
9
12
|
|
|
10
13
|
export default defineDirective({
|
|
11
14
|
mounted(element: HTMLElement, { value }) {
|
|
15
|
+
// TODO replace with argument when typed properly
|
|
16
|
+
const modifiers = { css: true, watch: true };
|
|
17
|
+
|
|
12
18
|
const listener = typeof value === 'function' ? (value as MeasureDirectiveListener) : null;
|
|
13
|
-
const
|
|
19
|
+
const update = () => {
|
|
20
|
+
const sizes = element.getBoundingClientRect();
|
|
21
|
+
|
|
22
|
+
if (modifiers.css) {
|
|
23
|
+
element.style.setProperty('--width', `${sizes.width}px`);
|
|
24
|
+
element.style.setProperty('--height', `${sizes.height}px`);
|
|
25
|
+
}
|
|
14
26
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
element.style.setProperty('--height', `${sizes.height}px`);
|
|
27
|
+
listener?.({ width: sizes.width, height: sizes.height });
|
|
28
|
+
};
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
if (modifiers.watch) {
|
|
31
|
+
resizeObservers.set(element, tap(new ResizeObserver(update)).observe(element));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
update();
|
|
35
|
+
},
|
|
36
|
+
unmounted(element) {
|
|
37
|
+
resizeObservers.get(element)?.unobserve(element);
|
|
38
|
+
resizeObservers.delete(element);
|
|
20
39
|
},
|
|
21
40
|
});
|
package/src/errors/Errors.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { JSError, facade, isObject, objectWithoutEmpty, toString } from '@noeldemartin/utils';
|
|
1
|
+
import { JSError, facade, isDevelopment, isObject, isTesting, objectWithoutEmpty, toString } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
|
-
import App from '
|
|
4
|
-
import ServiceBootError from '
|
|
5
|
-
import UI, { UIComponents } from '
|
|
6
|
-
import { translateWithDefault } from '
|
|
3
|
+
import App from '@aerogel/core/services/App';
|
|
4
|
+
import ServiceBootError from '@aerogel/core/errors/ServiceBootError';
|
|
5
|
+
import UI, { UIComponents } from '@aerogel/core/ui/UI';
|
|
6
|
+
import { translateWithDefault } from '@aerogel/core/lang/utils';
|
|
7
|
+
import { Colors } from '@aerogel/core/components/constants';
|
|
8
|
+
import { Events } from '@aerogel/core/services';
|
|
9
|
+
import type { ErrorReportModalProps } from '@aerogel/core/components/contracts/ErrorReportModal';
|
|
10
|
+
import type { ModalComponent } from '@aerogel/core/ui/UI.state';
|
|
7
11
|
|
|
8
12
|
import Service from './Errors.state';
|
|
9
|
-
import { Colors } from '@/components/constants';
|
|
10
|
-
import { Events } from '@/services';
|
|
11
|
-
import type { AGErrorReportModalProps } from '@/components/modals/AGErrorReportModal';
|
|
12
13
|
import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
13
|
-
import type { ModalComponent } from '@/ui/UI.state';
|
|
14
14
|
|
|
15
15
|
export class ErrorsService extends Service {
|
|
16
16
|
|
|
@@ -34,7 +34,7 @@ export class ErrorsService extends Service {
|
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
UI.openModal<ModalComponent<
|
|
37
|
+
UI.openModal<ModalComponent<ErrorReportModalProps>>(UI.requireComponent(UIComponents.ErrorReportModal), {
|
|
38
38
|
reports,
|
|
39
39
|
});
|
|
40
40
|
}
|
|
@@ -42,11 +42,11 @@ export class ErrorsService extends Service {
|
|
|
42
42
|
public async report(error: ErrorSource, message?: string): Promise<void> {
|
|
43
43
|
await Events.emit('error', { error, message });
|
|
44
44
|
|
|
45
|
-
if (
|
|
45
|
+
if (isTesting('unit')) {
|
|
46
46
|
throw error;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
if (
|
|
49
|
+
if (isDevelopment()) {
|
|
50
50
|
this.logError(error);
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -81,7 +81,7 @@ export class ErrorsService extends Service {
|
|
|
81
81
|
text: translateWithDefault('errors.viewDetails', 'View details'),
|
|
82
82
|
dismiss: true,
|
|
83
83
|
handler: () =>
|
|
84
|
-
UI.openModal<ModalComponent<
|
|
84
|
+
UI.openModal<ModalComponent<ErrorReportModalProps>>(
|
|
85
85
|
UI.requireComponent(UIComponents.ErrorReportModal),
|
|
86
86
|
{ reports: [report] },
|
|
87
87
|
),
|
|
@@ -178,7 +178,7 @@ export class ErrorsService extends Service {
|
|
|
178
178
|
|
|
179
179
|
export default facade(ErrorsService);
|
|
180
180
|
|
|
181
|
-
declare module '
|
|
181
|
+
declare module '@aerogel/core/services/Events' {
|
|
182
182
|
export interface EventsPayload {
|
|
183
183
|
error: { error: ErrorSource; message?: string };
|
|
184
184
|
}
|
package/src/errors/index.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { App } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { bootServices } from '
|
|
4
|
-
import { definePlugin } from '
|
|
3
|
+
import { bootServices } from '@aerogel/core/services';
|
|
4
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
5
5
|
|
|
6
6
|
import Errors from './Errors';
|
|
7
|
-
import { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
7
|
+
import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
8
8
|
|
|
9
9
|
export * from './utils';
|
|
10
|
-
export { Errors
|
|
10
|
+
export { Errors };
|
|
11
|
+
export { default as JobCancelledError } from './JobCancelledError';
|
|
12
|
+
export { default as ServiceBootError } from './ServiceBootError';
|
|
13
|
+
export type { ErrorSource, ErrorReport, ErrorReportLog };
|
|
11
14
|
|
|
12
15
|
const services = { $errors: Errors };
|
|
13
16
|
const frameworkHandler: ErrorHandler = (error) => {
|
|
@@ -35,12 +38,12 @@ export default definePlugin({
|
|
|
35
38
|
},
|
|
36
39
|
});
|
|
37
40
|
|
|
38
|
-
declare module '
|
|
41
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
39
42
|
export interface AerogelOptions {
|
|
40
43
|
handleError?(error: ErrorSource): boolean;
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
declare module '
|
|
47
|
+
declare module '@aerogel/core/services' {
|
|
45
48
|
export interface Services extends ErrorsServices {}
|
|
46
49
|
}
|
package/src/errors/utils.ts
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import { JSError, isObject, toString } from '@noeldemartin/utils';
|
|
2
|
-
import { translateWithDefault } from '
|
|
2
|
+
import { translateWithDefault } from '@aerogel/core/lang/utils';
|
|
3
3
|
import type { ErrorSource } from './Errors.state';
|
|
4
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
|
+
|
|
5
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
|
+
|
|
6
22
|
if (typeof error === 'string') {
|
|
7
23
|
return error;
|
|
8
24
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import { useForm } from '
|
|
4
|
-
import {
|
|
5
|
-
import { numberInput, requiredStringInput } from '@/forms/utils';
|
|
3
|
+
import { useForm } from '@aerogel/core/forms/composition';
|
|
4
|
+
import { numberInput, requiredStringInput } from '@aerogel/core/forms/utils';
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
import { FormFieldTypes } from './FormController';
|
|
7
|
+
|
|
8
|
+
describe('FormController', () => {
|
|
8
9
|
|
|
9
10
|
it('defines magic fields', () => {
|
|
10
11
|
const form = useForm({
|
|
@@ -55,4 +56,32 @@ describe('Form', () => {
|
|
|
55
56
|
expect(form.name).toBeNull();
|
|
56
57
|
});
|
|
57
58
|
|
|
59
|
+
it('trims values', () => {
|
|
60
|
+
// Arrange
|
|
61
|
+
const form = useForm({
|
|
62
|
+
trimmed: {
|
|
63
|
+
type: FormFieldTypes.String,
|
|
64
|
+
rules: 'required',
|
|
65
|
+
},
|
|
66
|
+
untrimmed: {
|
|
67
|
+
type: FormFieldTypes.String,
|
|
68
|
+
rules: 'required',
|
|
69
|
+
trim: false,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Act
|
|
74
|
+
form.trimmed = ' ';
|
|
75
|
+
form.untrimmed = ' ';
|
|
76
|
+
|
|
77
|
+
form.submit();
|
|
78
|
+
|
|
79
|
+
// Assert
|
|
80
|
+
expect(form.valid).toBe(false);
|
|
81
|
+
expect(form.submitted).toBe(true);
|
|
82
|
+
expect(form.trimmed).toEqual('');
|
|
83
|
+
expect(form.untrimmed).toEqual(' ');
|
|
84
|
+
expect(form.errors).toEqual({ trimmed: ['required'], untrimmed: null });
|
|
85
|
+
});
|
|
86
|
+
|
|
58
87
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { MagicObject, arrayRemove } from '@noeldemartin/utils';
|
|
2
1
|
import { computed, nextTick, reactive, readonly, ref } from 'vue';
|
|
2
|
+
import { MagicObject, arrayRemove, fail, toString } from '@noeldemartin/utils';
|
|
3
|
+
import { validate } from './validation';
|
|
3
4
|
import type { ObjectValues } from '@noeldemartin/utils';
|
|
4
5
|
import type { ComputedRef, DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
|
|
5
6
|
|
|
@@ -13,6 +14,7 @@ export const FormFieldTypes = {
|
|
|
13
14
|
|
|
14
15
|
export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
|
|
15
16
|
type: TType;
|
|
17
|
+
trim?: boolean;
|
|
16
18
|
default?: GetFormFieldValue<TType>;
|
|
17
19
|
rules?: TRules;
|
|
18
20
|
}
|
|
@@ -36,21 +38,21 @@ export type FormErrors<T> = {
|
|
|
36
38
|
export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.String
|
|
37
39
|
? string
|
|
38
40
|
: TType extends typeof FormFieldTypes.Number
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
? number
|
|
42
|
+
: TType extends typeof FormFieldTypes.Boolean
|
|
43
|
+
? boolean
|
|
44
|
+
: TType extends typeof FormFieldTypes.Object
|
|
45
|
+
? object
|
|
46
|
+
: TType extends typeof FormFieldTypes.Date
|
|
47
|
+
? Date
|
|
48
|
+
: never;
|
|
47
49
|
|
|
48
|
-
const validForms: WeakMap<
|
|
50
|
+
const validForms: WeakMap<FormController, ComputedRef<boolean>> = new WeakMap();
|
|
49
51
|
|
|
50
52
|
export type SubmitFormListener = () => unknown;
|
|
51
53
|
export type FocusFormListener = (input: string) => unknown;
|
|
52
54
|
|
|
53
|
-
export default class
|
|
55
|
+
export default class FormController<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
|
|
54
56
|
|
|
55
57
|
public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
|
|
56
58
|
|
|
@@ -85,7 +87,13 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
public setFieldValue<T extends keyof Fields>(field: T, value: FormData<Fields>[T]): void {
|
|
88
|
-
|
|
90
|
+
const definition =
|
|
91
|
+
this._fields[field] ?? fail<FormFieldDefinition>(`Trying to set undefined '${toString(field)}' field`);
|
|
92
|
+
|
|
93
|
+
this._data[field] =
|
|
94
|
+
definition.type === FormFieldTypes.String && (definition.trim ?? true)
|
|
95
|
+
? (toString(value).trim() as FormData<Fields>[T])
|
|
96
|
+
: value;
|
|
89
97
|
|
|
90
98
|
if (this._submitted.value) {
|
|
91
99
|
this.validate();
|
|
@@ -96,16 +104,23 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
96
104
|
return this._data[field] as unknown as GetFormFieldValue<Fields[T]['type']>;
|
|
97
105
|
}
|
|
98
106
|
|
|
107
|
+
public getFieldRules<T extends keyof Fields>(field: T): string[] {
|
|
108
|
+
return this._fields[field]?.rules?.split('|') ?? [];
|
|
109
|
+
}
|
|
110
|
+
|
|
99
111
|
public data(): FormData<Fields> {
|
|
100
112
|
return { ...this._data };
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
public validate(): boolean {
|
|
104
|
-
const errors = Object.entries(this._fields).reduce(
|
|
105
|
-
formErrors[name
|
|
116
|
+
const errors = Object.entries(this._fields).reduce(
|
|
117
|
+
(formErrors, [name, definition]) => {
|
|
118
|
+
formErrors[name] = this.getFieldErrors(name, definition);
|
|
106
119
|
|
|
107
|
-
|
|
108
|
-
|
|
120
|
+
return formErrors;
|
|
121
|
+
},
|
|
122
|
+
{} as Record<string, string[] | null>,
|
|
123
|
+
);
|
|
109
124
|
|
|
110
125
|
this.resetErrors(errors);
|
|
111
126
|
|
|
@@ -153,29 +168,35 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
153
168
|
this._listeners['focus']?.forEach((listener) => listener(input));
|
|
154
169
|
}
|
|
155
170
|
|
|
156
|
-
protected __get(property: string): unknown {
|
|
171
|
+
protected override __get(property: string): unknown {
|
|
157
172
|
if (!(property in this._fields)) {
|
|
158
173
|
return super.__get(property);
|
|
159
174
|
}
|
|
160
175
|
|
|
161
|
-
return this.
|
|
176
|
+
return this.getFieldValue(property);
|
|
162
177
|
}
|
|
163
178
|
|
|
164
|
-
protected __set(property: string, value: unknown): void {
|
|
179
|
+
protected override __set(property: string, value: unknown): void {
|
|
165
180
|
if (!(property in this._fields)) {
|
|
166
181
|
super.__set(property, value);
|
|
167
182
|
|
|
168
183
|
return;
|
|
169
184
|
}
|
|
170
185
|
|
|
171
|
-
|
|
186
|
+
this.setFieldValue(property, value as FormData<Fields>[string]);
|
|
172
187
|
}
|
|
173
188
|
|
|
174
189
|
private getFieldErrors(name: keyof Fields, definition: FormFieldDefinition): string[] | null {
|
|
175
190
|
const errors = [];
|
|
191
|
+
const value = this._data[name];
|
|
192
|
+
const rules = definition.rules?.split('|') ?? [];
|
|
193
|
+
|
|
194
|
+
for (const rule of rules) {
|
|
195
|
+
if (rule !== 'required' && (value === null || value === undefined)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
176
198
|
|
|
177
|
-
|
|
178
|
-
errors.push('required');
|
|
199
|
+
errors.push(...validate(value, rule));
|
|
179
200
|
}
|
|
180
201
|
|
|
181
202
|
return errors.length > 0 ? errors : null;
|
|
@@ -186,10 +207,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
186
207
|
return {} as FormData<Fields>;
|
|
187
208
|
}
|
|
188
209
|
|
|
189
|
-
const data = Object.entries(fields).reduce((
|
|
190
|
-
|
|
210
|
+
const data = Object.entries(fields).reduce((initialData, [name, definition]) => {
|
|
211
|
+
initialData[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
|
|
191
212
|
|
|
192
|
-
return
|
|
213
|
+
return initialData;
|
|
193
214
|
}, {} as FormData<Fields>);
|
|
194
215
|
|
|
195
216
|
return reactive(data) as FormData<Fields>;
|
package/src/forms/composition.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { FormData, FormFieldDefinitions } from '
|
|
1
|
+
import FormController from '@aerogel/core/forms/FormController';
|
|
2
|
+
import type { FormData, FormFieldDefinitions } from '@aerogel/core/forms/FormController';
|
|
3
3
|
|
|
4
|
-
export function useForm<const T extends FormFieldDefinitions>(fields: T):
|
|
5
|
-
return new
|
|
4
|
+
export function useForm<const T extends FormFieldDefinitions>(fields: T): FormController<T> & FormData<T> {
|
|
5
|
+
return new FormController(fields) as FormController<T> & FormData<T>;
|
|
6
6
|
}
|
package/src/forms/index.ts
CHANGED