@aerogel/core 0.0.0-next.8e6b2bcc764fa682decbb41aa6848c77a744dec3 → 0.0.0-next.906ec80f260b7e5cb54a0c97bd4905bdaf4bf916
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 +653 -91
- package/dist/aerogel-core.esm.js +1 -1
- package/dist/aerogel-core.esm.js.map +1 -1
- package/package.json +3 -3
- package/src/bootstrap/bootstrap.test.ts +0 -1
- package/src/bootstrap/index.ts +12 -2
- package/src/components/AGAppSnackbars.vue +1 -1
- package/src/components/composition.ts +23 -0
- package/src/components/forms/AGForm.vue +9 -10
- package/src/components/forms/AGInput.vue +2 -0
- package/src/components/headless/forms/AGHeadlessButton.ts +3 -0
- package/src/components/headless/forms/AGHeadlessButton.vue +15 -4
- package/src/components/headless/forms/AGHeadlessInput.ts +10 -4
- package/src/components/headless/forms/AGHeadlessInput.vue +18 -5
- package/src/components/headless/forms/AGHeadlessInputDescription.vue +28 -0
- package/src/components/headless/forms/AGHeadlessInputInput.vue +42 -5
- package/src/components/headless/forms/AGHeadlessInputTextArea.vue +43 -0
- package/src/components/headless/forms/composition.ts +10 -0
- package/src/components/headless/forms/index.ts +4 -0
- package/src/components/headless/modals/AGHeadlessModal.ts +3 -1
- package/src/components/headless/modals/AGHeadlessModal.vue +10 -4
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +10 -6
- package/src/components/headless/modals/AGHeadlessModalTitle.vue +14 -4
- package/src/components/index.ts +2 -0
- package/src/components/interfaces.ts +24 -0
- package/src/components/lib/AGMarkdown.vue +22 -4
- package/src/components/lib/AGMeasured.vue +1 -0
- package/src/components/lib/AGProgressBar.vue +55 -0
- package/src/components/lib/index.ts +1 -0
- package/src/components/modals/AGAlertModal.ts +5 -2
- package/src/components/modals/AGConfirmModal.ts +18 -3
- package/src/components/modals/AGConfirmModal.vue +3 -3
- package/src/components/modals/AGErrorReportModal.ts +5 -2
- package/src/components/modals/AGLoadingModal.ts +10 -4
- package/src/components/modals/AGModal.ts +1 -0
- package/src/components/modals/AGModalContext.vue +14 -4
- package/src/components/modals/AGPromptModal.ts +14 -3
- package/src/components/modals/AGPromptModal.vue +2 -2
- package/src/directives/measure.ts +24 -5
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +2 -0
- package/src/errors/utils.ts +16 -0
- package/src/forms/Form.test.ts +28 -0
- package/src/forms/Form.ts +65 -8
- package/src/forms/index.ts +3 -1
- package/src/forms/utils.ts +34 -3
- package/src/forms/validation.ts +19 -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 +43 -0
- package/src/lang/Lang.state.ts +11 -0
- package/src/lang/Lang.ts +48 -21
- package/src/services/App.state.ts +22 -0
- package/src/services/App.ts +8 -0
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.ts +13 -3
- package/src/services/Service.ts +116 -45
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +9 -2
- package/src/services/utils.ts +18 -0
- package/src/testing/setup.ts +27 -0
- package/src/ui/UI.state.ts +7 -0
- package/src/ui/UI.ts +145 -44
- package/src/ui/index.ts +1 -0
- package/src/ui/utils.ts +16 -0
- 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 +17 -2
- package/src/utils/vue.ts +15 -3
- package/vite.config.ts +4 -1
|
@@ -3,20 +3,23 @@
|
|
|
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
|
+
import { isInstanceOf } from '@noeldemartin/utils';
|
|
7
8
|
|
|
8
9
|
import { renderMarkdown } from '@/utils/markdown';
|
|
9
|
-
import { booleanProp, objectProp, stringProp } from '@/utils/vue';
|
|
10
|
+
import { booleanProp, mixedProp, objectProp, stringProp } from '@/utils/vue';
|
|
10
11
|
import { translate } from '@/lang';
|
|
11
12
|
|
|
12
13
|
const props = defineProps({
|
|
13
14
|
as: stringProp(),
|
|
14
15
|
inline: booleanProp(),
|
|
15
16
|
langKey: stringProp(),
|
|
16
|
-
langParams:
|
|
17
|
+
langParams: mixedProp<number | Record<string, unknown>>(),
|
|
17
18
|
text: stringProp(),
|
|
19
|
+
actions: objectProp<Record<string, () => unknown>>(),
|
|
18
20
|
});
|
|
19
21
|
|
|
22
|
+
const attrs = useAttrs();
|
|
20
23
|
const markdown = computed(() => props.text ?? (props.langKey && translate(props.langKey, props.langParams ?? {})));
|
|
21
24
|
const html = computed(() => {
|
|
22
25
|
if (!markdown.value) {
|
|
@@ -32,5 +35,20 @@ const html = computed(() => {
|
|
|
32
35
|
return renderedHtml;
|
|
33
36
|
});
|
|
34
37
|
const root = () =>
|
|
35
|
-
h(props.as ?? (props.inline ? 'span' : 'div'), {
|
|
38
|
+
h(props.as ?? (props.inline ? 'span' : 'div'), {
|
|
39
|
+
innerHTML: html.value,
|
|
40
|
+
onClick,
|
|
41
|
+
...attrs,
|
|
42
|
+
class: `${attrs.class ?? ''} ${props.inline ? '' : 'prose'}`,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
async function onClick(event: Event) {
|
|
46
|
+
const { target } = event;
|
|
47
|
+
|
|
48
|
+
if (isInstanceOf(target, HTMLElement) && target.dataset.markdownAction) {
|
|
49
|
+
props.actions?.[target.dataset.markdownAction]?.();
|
|
50
|
+
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
36
54
|
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-1 h-2 w-full overflow-hidden rounded-full bg-gray-200">
|
|
3
|
+
<div :class="barClasses" :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 { numberProp, objectProp, stringProp } from '@/utils/vue';
|
|
18
|
+
import type { Job } from '@/jobs';
|
|
19
|
+
|
|
20
|
+
const props = defineProps({
|
|
21
|
+
progress: numberProp(),
|
|
22
|
+
barClass: stringProp(''),
|
|
23
|
+
job: objectProp<Job>(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let cleanup: Function | undefined;
|
|
27
|
+
const jobProgress = ref(0);
|
|
28
|
+
const barClasses = computed(() => {
|
|
29
|
+
const classes = props.barClass ?? '';
|
|
30
|
+
|
|
31
|
+
return `h-full w-full transition-transform duration-500 ease-linear ${
|
|
32
|
+
classes.includes('bg-') ? classes : `${classes} bg-gray-700`
|
|
33
|
+
}`;
|
|
34
|
+
});
|
|
35
|
+
const renderedProgress = computed(() => {
|
|
36
|
+
if (typeof props.progress === 'number') {
|
|
37
|
+
return props.progress;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return jobProgress.value;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
watch(
|
|
44
|
+
() => props.job,
|
|
45
|
+
() => {
|
|
46
|
+
cleanup?.();
|
|
47
|
+
|
|
48
|
+
jobProgress.value = props.job?.progress ?? 0;
|
|
49
|
+
cleanup = props.job?.listeners.add({ onUpdated: (progress) => (jobProgress.value = progress) });
|
|
50
|
+
},
|
|
51
|
+
{ immediate: true },
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
onUnmounted(() => cleanup?.());
|
|
55
|
+
</script>
|
|
@@ -2,4 +2,5 @@ export { default as AGErrorMessage } from './AGErrorMessage.vue';
|
|
|
2
2
|
export { default as AGLink } from './AGLink.vue';
|
|
3
3
|
export { default as AGMarkdown } from './AGMarkdown.vue';
|
|
4
4
|
export { default as AGMeasured } from './AGMeasured.vue';
|
|
5
|
+
export { default as AGProgressBar } from './AGProgressBar.vue';
|
|
5
6
|
export { default as AGStartupCrash } from './AGStartupCrash.vue';
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { ExtractPropTypes } from 'vue';
|
|
2
|
-
import type {
|
|
2
|
+
import type { ObjectWithout, Pretty } from '@noeldemartin/utils';
|
|
3
3
|
|
|
4
4
|
import { requiredStringProp, stringProp } from '@/utils';
|
|
5
|
+
import type { AcceptRefs } from '@/utils';
|
|
5
6
|
|
|
6
7
|
export const alertModalProps = {
|
|
7
8
|
title: stringProp(),
|
|
8
9
|
message: requiredStringProp(),
|
|
9
10
|
};
|
|
10
11
|
|
|
11
|
-
export type AGAlertModalProps =
|
|
12
|
+
export type AGAlertModalProps = Pretty<
|
|
13
|
+
AcceptRefs<ObjectWithout<ExtractPropTypes<typeof alertModalProps>, null | undefined>>
|
|
14
|
+
>;
|
|
12
15
|
|
|
13
16
|
export function useAlertModalProps(): typeof alertModalProps {
|
|
14
17
|
return alertModalProps;
|
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import { computed } from 'vue';
|
|
2
2
|
import type { ExtractPropTypes } from 'vue';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ObjectWithout, Pretty, SubPartial } from '@noeldemartin/utils';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Colors } from '@/components/constants';
|
|
6
|
+
import { booleanProp, enumProp, objectProp, requiredStringProp, stringProp } from '@/utils';
|
|
6
7
|
import { translateWithDefault } from '@/lang';
|
|
8
|
+
import type { AcceptRefs } from '@/utils';
|
|
9
|
+
import type { ConfirmCheckboxes } from '@/ui';
|
|
7
10
|
|
|
8
11
|
export const confirmModalProps = {
|
|
9
12
|
title: stringProp(),
|
|
10
13
|
message: requiredStringProp(),
|
|
11
14
|
acceptText: stringProp(),
|
|
15
|
+
acceptColor: enumProp(Colors, Colors.Primary),
|
|
12
16
|
cancelText: stringProp(),
|
|
17
|
+
cancelColor: enumProp(Colors, Colors.Clear),
|
|
18
|
+
checkboxes: objectProp<ConfirmCheckboxes>(),
|
|
19
|
+
actions: objectProp<Record<string, () => unknown>>(),
|
|
20
|
+
required: booleanProp(false),
|
|
13
21
|
};
|
|
14
22
|
|
|
15
|
-
export type AGConfirmModalProps =
|
|
23
|
+
export type AGConfirmModalProps = Pretty<
|
|
24
|
+
AcceptRefs<
|
|
25
|
+
SubPartial<
|
|
26
|
+
ObjectWithout<ExtractPropTypes<typeof confirmModalProps>, null | undefined>,
|
|
27
|
+
'acceptColor' | 'cancelColor'
|
|
28
|
+
>
|
|
29
|
+
>
|
|
30
|
+
>;
|
|
16
31
|
|
|
17
32
|
export function useConfirmModalProps(): typeof confirmModalProps {
|
|
18
33
|
return confirmModalProps;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<AGModal v-slot="{ close }: IAGModalDefaultSlotProps" :cancellable="false" :title="title">
|
|
3
|
-
<AGMarkdown :text="message" />
|
|
3
|
+
<AGMarkdown :text="message" :actions="actions" />
|
|
4
4
|
|
|
5
5
|
<div class="mt-2 flex flex-row-reverse gap-2">
|
|
6
|
-
<AGButton @click="close(true)">
|
|
6
|
+
<AGButton :color="acceptColor" @click="close(true)">
|
|
7
7
|
{{ renderedAcceptText }}
|
|
8
8
|
</AGButton>
|
|
9
|
-
<AGButton color="
|
|
9
|
+
<AGButton v-if="!required" :color="cancelColor" @click="close()">
|
|
10
10
|
{{ renderedCancelText }}
|
|
11
11
|
</AGButton>
|
|
12
12
|
</div>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { computed, ref } from 'vue';
|
|
2
2
|
import type { Component, ExtractPropTypes } from 'vue';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ObjectWithout, Pretty } from '@noeldemartin/utils';
|
|
4
4
|
|
|
5
5
|
import { requiredArrayProp } from '@/utils/vue';
|
|
6
6
|
import { translateWithDefault } from '@/lang';
|
|
7
|
+
import type { AcceptRefs } from '@/utils/vue';
|
|
7
8
|
import type { ErrorReport } from '@/errors';
|
|
8
9
|
|
|
9
10
|
export interface IAGErrorReportModalButtonsDefaultSlotProps {
|
|
@@ -18,7 +19,9 @@ export const errorReportModalProps = {
|
|
|
18
19
|
reports: requiredArrayProp<ErrorReport>(),
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
export type AGErrorReportModalProps =
|
|
22
|
+
export type AGErrorReportModalProps = Pretty<
|
|
23
|
+
AcceptRefs<ObjectWithout<ExtractPropTypes<typeof errorReportModalProps>, null | undefined>>
|
|
24
|
+
>;
|
|
22
25
|
|
|
23
26
|
export function useErrorReportModalProps(): typeof errorReportModalProps {
|
|
24
27
|
return errorReportModalProps;
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { computed } from 'vue';
|
|
2
2
|
import type { ExtractPropTypes } from 'vue';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ObjectWithout } from '@noeldemartin/utils';
|
|
4
4
|
|
|
5
|
-
import { stringProp } from '@/utils';
|
|
5
|
+
import { numberProp, stringProp } from '@/utils';
|
|
6
6
|
import { translateWithDefault } from '@/lang';
|
|
7
|
+
import type { AcceptRefs } from '@/utils';
|
|
7
8
|
|
|
8
9
|
export const loadingModalProps = {
|
|
10
|
+
title: stringProp(),
|
|
9
11
|
message: stringProp(),
|
|
12
|
+
progress: numberProp(),
|
|
10
13
|
};
|
|
11
14
|
|
|
12
|
-
export type AGLoadingModalProps =
|
|
15
|
+
export type AGLoadingModalProps = AcceptRefs<
|
|
16
|
+
ObjectWithout<ExtractPropTypes<typeof loadingModalProps>, null | undefined>
|
|
17
|
+
>;
|
|
13
18
|
|
|
14
19
|
export function useLoadingModalProps(): typeof loadingModalProps {
|
|
15
20
|
return loadingModalProps;
|
|
@@ -18,6 +23,7 @@ export function useLoadingModalProps(): typeof loadingModalProps {
|
|
|
18
23
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
19
24
|
export function useLoadingModal(props: ExtractPropTypes<typeof loadingModalProps>) {
|
|
20
25
|
const renderedMessage = computed(() => props.message ?? translateWithDefault('ui.loading', 'Loading...'));
|
|
26
|
+
const showProgress = computed(() => typeof props.progress === 'number');
|
|
21
27
|
|
|
22
|
-
return { renderedMessage };
|
|
28
|
+
return { renderedMessage, showProgress };
|
|
23
29
|
}
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<component :is="modal.component" v-bind="
|
|
2
|
+
<component :is="modal.component" v-bind="modalProperties" />
|
|
3
3
|
</template>
|
|
4
4
|
|
|
5
5
|
<script setup lang="ts">
|
|
6
|
-
import { provide, toRef } from 'vue';
|
|
6
|
+
import { computed, provide, toRef, unref } from 'vue';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { numberProp, requiredObjectProp } from '@/utils/vue';
|
|
9
9
|
import type { Modal } from '@/ui/UI.state';
|
|
10
10
|
|
|
11
11
|
import type { IAGModalContext } from './AGModalContext';
|
|
12
12
|
|
|
13
13
|
const props = defineProps({
|
|
14
14
|
modal: requiredObjectProp<Modal>(),
|
|
15
|
-
childIndex:
|
|
15
|
+
childIndex: numberProp(0),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const modalProperties = computed(() => {
|
|
19
|
+
const properties = {} as typeof props.modal.properties;
|
|
20
|
+
|
|
21
|
+
for (const property in props.modal.properties) {
|
|
22
|
+
properties[property] = unref(props.modal.properties[property]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return properties;
|
|
16
26
|
});
|
|
17
27
|
|
|
18
28
|
provide<IAGModalContext>('modal', {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { computed } from 'vue';
|
|
2
2
|
import type { ExtractPropTypes } from 'vue';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ObjectWithout, Pretty, SubPartial } from '@noeldemartin/utils';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Colors } from '@/components/constants';
|
|
6
|
+
import { enumProp, requiredStringProp, stringProp } from '@/utils';
|
|
6
7
|
import { translateWithDefault } from '@/lang';
|
|
8
|
+
import type { AcceptRefs } from '@/utils';
|
|
7
9
|
|
|
8
10
|
export const promptModalProps = {
|
|
9
11
|
title: stringProp(),
|
|
@@ -12,10 +14,19 @@ export const promptModalProps = {
|
|
|
12
14
|
defaultValue: stringProp(),
|
|
13
15
|
placeholder: stringProp(),
|
|
14
16
|
acceptText: stringProp(),
|
|
17
|
+
acceptColor: enumProp(Colors, Colors.Primary),
|
|
15
18
|
cancelText: stringProp(),
|
|
19
|
+
cancelColor: enumProp(Colors, Colors.Clear),
|
|
16
20
|
};
|
|
17
21
|
|
|
18
|
-
export type AGPromptModalProps =
|
|
22
|
+
export type AGPromptModalProps = Pretty<
|
|
23
|
+
AcceptRefs<
|
|
24
|
+
SubPartial<
|
|
25
|
+
ObjectWithout<ExtractPropTypes<typeof promptModalProps>, null | undefined>,
|
|
26
|
+
'acceptColor' | 'cancelColor'
|
|
27
|
+
>
|
|
28
|
+
>
|
|
29
|
+
>;
|
|
19
30
|
|
|
20
31
|
export function usePromptModalProps(): typeof promptModalProps {
|
|
21
32
|
return promptModalProps;
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
<AGInput name="draft" :placeholder="placeholder" :label="label" />
|
|
7
7
|
|
|
8
8
|
<div class="mt-2 flex flex-row-reverse gap-2">
|
|
9
|
-
<AGButton submit>
|
|
9
|
+
<AGButton :color="acceptColor" submit>
|
|
10
10
|
{{ renderedAcceptText }}
|
|
11
11
|
</AGButton>
|
|
12
|
-
<AGButton color="
|
|
12
|
+
<AGButton :color="cancelColor" @click="close()">
|
|
13
13
|
{{ renderedCancelText }}
|
|
14
14
|
</AGButton>
|
|
15
15
|
</div>
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { defineDirective } from '@/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/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
|
8
8
|
|
|
9
9
|
export * from './utils';
|
|
10
10
|
export { Errors, ErrorSource, ErrorReport, ErrorReportLog };
|
|
11
|
+
export { default as JobCancelledError } from './JobCancelledError';
|
|
12
|
+
export { default as ServiceBootError } from './ServiceBootError';
|
|
11
13
|
|
|
12
14
|
const services = { $errors: Errors };
|
|
13
15
|
const frameworkHandler: ErrorHandler = (error) => {
|
package/src/errors/utils.ts
CHANGED
|
@@ -2,7 +2,23 @@ import { JSError, isObject, toString } from '@noeldemartin/utils';
|
|
|
2
2
|
import { translateWithDefault } from '@/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
|
}
|
package/src/forms/Form.test.ts
CHANGED
|
@@ -55,4 +55,32 @@ describe('Form', () => {
|
|
|
55
55
|
expect(form.name).toBeNull();
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
it('trims values', () => {
|
|
59
|
+
// Arrange
|
|
60
|
+
const form = useForm({
|
|
61
|
+
trimmed: {
|
|
62
|
+
type: FormFieldTypes.String,
|
|
63
|
+
rules: 'required',
|
|
64
|
+
},
|
|
65
|
+
untrimmed: {
|
|
66
|
+
type: FormFieldTypes.String,
|
|
67
|
+
rules: 'required',
|
|
68
|
+
trim: false,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Act
|
|
73
|
+
form.trimmed = ' ';
|
|
74
|
+
form.untrimmed = ' ';
|
|
75
|
+
|
|
76
|
+
form.submit();
|
|
77
|
+
|
|
78
|
+
// Assert
|
|
79
|
+
expect(form.valid).toBe(false);
|
|
80
|
+
expect(form.submitted).toBe(true);
|
|
81
|
+
expect(form.trimmed).toEqual('');
|
|
82
|
+
expect(form.untrimmed).toEqual(' ');
|
|
83
|
+
expect(form.errors).toEqual({ trimmed: ['required'], untrimmed: null });
|
|
84
|
+
});
|
|
85
|
+
|
|
58
86
|
});
|
package/src/forms/Form.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
|
@@ -8,10 +9,12 @@ export const FormFieldTypes = {
|
|
|
8
9
|
Number: 'number',
|
|
9
10
|
Boolean: 'boolean',
|
|
10
11
|
Object: 'object',
|
|
12
|
+
Date: 'date',
|
|
11
13
|
} as const;
|
|
12
14
|
|
|
13
15
|
export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
|
|
14
16
|
type: TType;
|
|
17
|
+
trim?: boolean;
|
|
15
18
|
default?: GetFormFieldValue<TType>;
|
|
16
19
|
rules?: TRules;
|
|
17
20
|
}
|
|
@@ -40,10 +43,15 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
|
|
|
40
43
|
? boolean
|
|
41
44
|
: TType extends typeof FormFieldTypes.Object
|
|
42
45
|
? object
|
|
46
|
+
: TType extends typeof FormFieldTypes.Date
|
|
47
|
+
? Date
|
|
43
48
|
: never;
|
|
44
49
|
|
|
45
50
|
const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
|
|
46
51
|
|
|
52
|
+
export type SubmitFormListener = () => unknown;
|
|
53
|
+
export type FocusFormListener = (input: string) => unknown;
|
|
54
|
+
|
|
47
55
|
export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
|
|
48
56
|
|
|
49
57
|
public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
|
|
@@ -52,6 +60,7 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
52
60
|
private _data: FormData<Fields>;
|
|
53
61
|
private _submitted: Ref<boolean>;
|
|
54
62
|
private _errors: FormErrors<Fields>;
|
|
63
|
+
private _listeners: { focus?: FocusFormListener[]; submit?: SubmitFormListener[] } = {};
|
|
55
64
|
|
|
56
65
|
constructor(fields: Fields) {
|
|
57
66
|
super();
|
|
@@ -78,7 +87,13 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
public setFieldValue<T extends keyof Fields>(field: T, value: FormData<Fields>[T]): void {
|
|
81
|
-
|
|
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;
|
|
82
97
|
|
|
83
98
|
if (this._submitted.value) {
|
|
84
99
|
this.validate();
|
|
@@ -89,6 +104,14 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
89
104
|
return this._data[field] as unknown as GetFormFieldValue<Fields[T]['type']>;
|
|
90
105
|
}
|
|
91
106
|
|
|
107
|
+
public getFieldRules<T extends keyof Fields>(field: T): string[] {
|
|
108
|
+
return this._fields[field]?.rules?.split('|') ?? [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public data(): FormData<Fields> {
|
|
112
|
+
return { ...this._data };
|
|
113
|
+
}
|
|
114
|
+
|
|
92
115
|
public validate(): boolean {
|
|
93
116
|
const errors = Object.entries(this._fields).reduce((formErrors, [name, definition]) => {
|
|
94
117
|
formErrors[name] = this.getFieldErrors(name, definition);
|
|
@@ -111,7 +134,35 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
111
134
|
public submit(): boolean {
|
|
112
135
|
this._submitted.value = true;
|
|
113
136
|
|
|
114
|
-
|
|
137
|
+
const valid = this.validate();
|
|
138
|
+
|
|
139
|
+
valid && this._listeners['submit']?.forEach((listener) => listener());
|
|
140
|
+
|
|
141
|
+
return valid;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public on(event: 'focus', listener: FocusFormListener): () => void;
|
|
145
|
+
public on(event: 'submit', listener: SubmitFormListener): () => void;
|
|
146
|
+
public on(event: 'focus' | 'submit', listener: FocusFormListener | SubmitFormListener): () => void {
|
|
147
|
+
this._listeners[event] ??= [];
|
|
148
|
+
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
150
|
+
this._listeners[event]?.push(listener as any);
|
|
151
|
+
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
|
+
return () => this.off(event as any, listener);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public off(event: 'focus', listener: FocusFormListener): void;
|
|
157
|
+
public off(event: 'submit', listener: SubmitFormListener): void;
|
|
158
|
+
public off(event: 'focus' | 'submit', listener: FocusFormListener | SubmitFormListener): void {
|
|
159
|
+
arrayRemove(this._listeners[event] ?? [], listener);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public async focus(input: string): Promise<void> {
|
|
163
|
+
await nextTick();
|
|
164
|
+
|
|
165
|
+
this._listeners['focus']?.forEach((listener) => listener(input));
|
|
115
166
|
}
|
|
116
167
|
|
|
117
168
|
protected __get(property: string): unknown {
|
|
@@ -119,7 +170,7 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
119
170
|
return super.__get(property);
|
|
120
171
|
}
|
|
121
172
|
|
|
122
|
-
return this.
|
|
173
|
+
return this.getFieldValue(property);
|
|
123
174
|
}
|
|
124
175
|
|
|
125
176
|
protected __set(property: string, value: unknown): void {
|
|
@@ -129,14 +180,20 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
129
180
|
return;
|
|
130
181
|
}
|
|
131
182
|
|
|
132
|
-
|
|
183
|
+
this.setFieldValue(property, value as FormData<Fields>[string]);
|
|
133
184
|
}
|
|
134
185
|
|
|
135
186
|
private getFieldErrors(name: keyof Fields, definition: FormFieldDefinition): string[] | null {
|
|
136
187
|
const errors = [];
|
|
188
|
+
const value = this._data[name];
|
|
189
|
+
const rules = definition.rules?.split('|') ?? [];
|
|
190
|
+
|
|
191
|
+
for (const rule of rules) {
|
|
192
|
+
if (rule !== 'required' && (value === null || value === undefined)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
137
195
|
|
|
138
|
-
|
|
139
|
-
errors.push('required');
|
|
196
|
+
errors.push(...validate(value, rule));
|
|
140
197
|
}
|
|
141
198
|
|
|
142
199
|
return errors.length > 0 ? errors : null;
|