@aerogel/core 0.0.0-next.c8f032a868370824898e171969aec1bb6827688e → 0.0.0-next.d197d66a9d339318d752a1d8a96b2919faba3003
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 +2310 -574
- package/dist/aerogel-core.js +3268 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +38 -34
- package/src/bootstrap/bootstrap.test.ts +7 -10
- package/src/bootstrap/index.ts +41 -9
- package/src/bootstrap/options.ts +4 -1
- package/src/components/AppLayout.vue +14 -0
- package/src/components/AppModals.vue +14 -0
- package/src/components/AppOverlays.vue +9 -0
- package/src/components/AppToasts.vue +16 -0
- package/src/components/contracts/AlertModal.ts +8 -0
- package/src/components/contracts/Button.ts +16 -0
- package/src/components/contracts/ConfirmModal.ts +46 -0
- package/src/components/contracts/DropdownMenu.ts +20 -0
- package/src/components/contracts/ErrorReportModal.ts +32 -0
- package/src/components/contracts/Input.ts +26 -0
- package/src/components/contracts/LoadingModal.ts +26 -0
- package/src/components/contracts/Modal.ts +21 -0
- package/src/components/contracts/PromptModal.ts +34 -0
- package/src/components/contracts/Select.ts +45 -0
- package/src/components/contracts/Toast.ts +15 -0
- package/src/components/contracts/index.ts +11 -0
- package/src/components/headless/HeadlessButton.vue +51 -0
- package/src/components/headless/HeadlessInput.vue +59 -0
- package/src/components/headless/HeadlessInputDescription.vue +27 -0
- package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
- package/src/components/headless/HeadlessInputInput.vue +75 -0
- package/src/components/headless/HeadlessInputLabel.vue +18 -0
- package/src/components/headless/HeadlessInputTextArea.vue +40 -0
- package/src/components/headless/HeadlessModal.vue +57 -0
- package/src/components/headless/HeadlessModalContent.vue +30 -0
- package/src/components/headless/HeadlessModalDescription.vue +12 -0
- package/src/components/headless/HeadlessModalOverlay.vue +12 -0
- package/src/components/headless/HeadlessModalTitle.vue +12 -0
- package/src/components/headless/HeadlessSelect.vue +120 -0
- package/src/components/headless/HeadlessSelectError.vue +25 -0
- package/src/components/headless/HeadlessSelectLabel.vue +25 -0
- package/src/components/headless/HeadlessSelectOption.vue +34 -0
- package/src/components/headless/HeadlessSelectOptions.vue +42 -0
- package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
- package/src/components/headless/HeadlessSelectValue.vue +18 -0
- package/src/components/headless/HeadlessToast.vue +18 -0
- package/src/components/headless/HeadlessToastAction.vue +13 -0
- package/src/components/headless/index.ts +19 -2
- package/src/components/index.ts +6 -7
- package/src/components/ui/AdvancedOptions.vue +18 -0
- package/src/components/ui/AlertModal.vue +14 -0
- package/src/components/ui/Button.vue +98 -0
- package/src/components/ui/Checkbox.vue +56 -0
- package/src/components/ui/ConfirmModal.vue +45 -0
- package/src/components/ui/DropdownMenu.vue +32 -0
- package/src/components/ui/DropdownMenuOption.vue +14 -0
- package/src/components/ui/DropdownMenuOptions.vue +27 -0
- package/src/components/ui/EditableContent.vue +82 -0
- package/src/components/ui/ErrorMessage.vue +15 -0
- package/src/components/ui/ErrorReportModal.vue +67 -0
- package/src/components/ui/ErrorReportModalButtons.vue +118 -0
- package/src/components/ui/ErrorReportModalTitle.vue +24 -0
- package/src/components/ui/Form.vue +24 -0
- package/src/components/ui/Input.vue +56 -0
- package/src/components/ui/Link.vue +12 -0
- package/src/components/ui/LoadingModal.vue +34 -0
- package/src/components/ui/Markdown.vue +69 -0
- package/src/components/ui/Modal.vue +122 -0
- package/src/components/ui/ModalContext.vue +31 -0
- package/src/components/ui/ProgressBar.vue +51 -0
- package/src/components/ui/PromptModal.vue +38 -0
- package/src/components/ui/Select.vue +27 -0
- package/src/components/ui/SelectLabel.vue +17 -0
- package/src/components/ui/SelectOption.vue +29 -0
- package/src/components/ui/SelectOptions.vue +35 -0
- package/src/components/ui/SelectTrigger.vue +29 -0
- package/src/components/ui/SettingsModal.vue +15 -0
- package/src/components/ui/StartupCrash.vue +31 -0
- package/src/components/ui/Toast.vue +44 -0
- package/src/components/ui/index.ts +30 -0
- package/src/directives/index.ts +27 -6
- package/src/directives/measure.ts +40 -0
- package/src/errors/Errors.state.ts +31 -0
- package/src/errors/Errors.ts +176 -0
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +49 -0
- package/src/errors/utils.ts +35 -0
- package/src/forms/FormController.test.ts +110 -0
- package/src/forms/FormController.ts +246 -0
- package/src/forms/index.ts +3 -2
- package/src/forms/utils.ts +62 -14
- package/src/forms/validation.ts +19 -0
- package/src/index.css +72 -0
- package/src/{main.ts → index.ts} +4 -2
- package/src/jobs/Job.ts +147 -0
- package/src/jobs/index.ts +10 -0
- package/src/jobs/listeners.ts +3 -0
- package/src/jobs/status.ts +4 -0
- package/src/lang/DefaultLangProvider.ts +46 -0
- package/src/lang/Lang.state.ts +11 -0
- package/src/lang/Lang.ts +45 -22
- package/src/lang/index.ts +14 -10
- package/src/lang/settings/Language.vue +48 -0
- package/src/lang/settings/index.ts +10 -0
- package/src/lang/utils.ts +4 -0
- package/src/plugins/Plugin.ts +2 -1
- package/src/plugins/index.ts +22 -0
- package/src/services/App.state.ts +43 -3
- package/src/services/App.ts +59 -3
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.test.ts +39 -0
- package/src/services/Events.ts +110 -36
- package/src/services/Service.ts +245 -53
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +36 -7
- package/src/services/store.ts +30 -0
- package/src/services/utils.ts +18 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/setup.ts +11 -0
- package/src/ui/UI.state.ts +21 -9
- package/src/ui/UI.ts +327 -64
- package/src/ui/index.ts +33 -22
- package/src/ui/utils.ts +16 -0
- package/src/utils/classes.ts +49 -0
- package/src/utils/composition/events.ts +4 -5
- package/src/utils/composition/forms.ts +27 -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 +33 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/markdown.test.ts +50 -0
- package/src/utils/markdown.ts +26 -4
- package/src/utils/types.ts +3 -0
- package/src/utils/vue.ts +28 -116
- package/.eslintrc.js +0 -3
- 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/noeldemartin.config.js +0 -2
- package/src/components/AGAppLayout.vue +0 -11
- package/src/components/AGAppOverlays.vue +0 -39
- package/src/components/basic/AGMarkdown.vue +0 -35
- package/src/components/basic/index.ts +0 -3
- package/src/components/forms/AGButton.vue +0 -21
- package/src/components/forms/AGForm.vue +0 -26
- package/src/components/forms/AGInput.vue +0 -32
- package/src/components/forms/index.ts +0 -5
- package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
- package/src/components/headless/forms/AGHeadlessInput.ts +0 -8
- package/src/components/headless/forms/AGHeadlessInput.vue +0 -54
- package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -33
- package/src/components/headless/forms/AGHeadlessInputLabel.vue +0 -16
- package/src/components/headless/forms/index.ts +0 -6
- package/src/components/headless/modals/AGHeadlessModal.ts +0 -7
- package/src/components/headless/modals/AGHeadlessModal.vue +0 -88
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -24
- package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
- package/src/components/headless/modals/index.ts +0 -6
- package/src/components/modals/AGAlertModal.vue +0 -26
- package/src/components/modals/AGConfirmModal.vue +0 -30
- package/src/components/modals/AGModal.ts +0 -10
- package/src/components/modals/AGModal.vue +0 -18
- package/src/components/modals/AGModalContext.ts +0 -8
- package/src/components/modals/AGModalContext.vue +0 -22
- package/src/components/modals/index.ts +0 -7
- package/src/directives/initial-focus.ts +0 -11
- package/src/forms/Form.test.ts +0 -37
- package/src/forms/Form.ts +0 -166
- package/src/forms/composition.ts +0 -6
- package/src/globals.ts +0 -6
- package/tsconfig.json +0 -10
- package/vite.config.ts +0 -13
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<HeadlessSelectOptions :class="renderedClasses">
|
|
3
|
+
<slot v-if="select?.options?.length">
|
|
4
|
+
<SelectOption v-for="option of select?.options ?? []" :key="option.key" :value="option.value">
|
|
5
|
+
{{ option.label }}
|
|
6
|
+
</SelectOption>
|
|
7
|
+
</slot>
|
|
8
|
+
<slot v-else name="empty">
|
|
9
|
+
<SelectOption disabled :value="null">
|
|
10
|
+
{{ $td('ui.selectEmpty', 'No options available') }}
|
|
11
|
+
</SelectOption>
|
|
12
|
+
</slot>
|
|
13
|
+
</HeadlessSelectOptions>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { computed } from 'vue';
|
|
18
|
+
import type { HTMLAttributes } from 'vue';
|
|
19
|
+
|
|
20
|
+
import HeadlessSelectOptions from '@aerogel/core/components/headless/HeadlessSelectOptions.vue';
|
|
21
|
+
import { classes, injectReactiveOrFail } from '@aerogel/core/utils';
|
|
22
|
+
import type { SelectExpose } from '@aerogel/core/components/contracts/Select';
|
|
23
|
+
|
|
24
|
+
import SelectOption from './SelectOption.vue';
|
|
25
|
+
|
|
26
|
+
const { class: rootClasses } = defineProps<{ class?: HTMLAttributes['class'] }>();
|
|
27
|
+
|
|
28
|
+
const select = injectReactiveOrFail<SelectExpose>('select', '<SelectOptions> must be a child of a <Select>');
|
|
29
|
+
const renderedClasses = computed(() =>
|
|
30
|
+
classes(
|
|
31
|
+
'z-50 overflow-auto rounded-lg bg-white text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden',
|
|
32
|
+
select.optionsClass,
|
|
33
|
+
rootClasses,
|
|
34
|
+
));
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<HeadlessSelectTrigger :class="renderedClasses">
|
|
3
|
+
<HeadlessSelectValue class="col-start-1 row-start-1 truncate pr-6" />
|
|
4
|
+
<IconCheveronDown class="col-start-1 row-start-1 size-5 self-center justify-self-end text-gray-500 sm:size-4" />
|
|
5
|
+
</HeadlessSelectTrigger>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import IconCheveronDown from '~icons/zondicons/cheveron-down';
|
|
10
|
+
|
|
11
|
+
import { computed } from 'vue';
|
|
12
|
+
import type { HTMLAttributes } from 'vue';
|
|
13
|
+
|
|
14
|
+
import HeadlessSelectTrigger from '@aerogel/core/components/headless/HeadlessSelectTrigger.vue';
|
|
15
|
+
import HeadlessSelectValue from '@aerogel/core/components/headless/HeadlessSelectValue.vue';
|
|
16
|
+
import { injectReactiveOrFail } from '@aerogel/core/utils';
|
|
17
|
+
import { classes } from '@aerogel/core/utils/classes';
|
|
18
|
+
import type { SelectExpose } from '@aerogel/core/components/contracts/Select';
|
|
19
|
+
|
|
20
|
+
const { class: rootClasses } = defineProps<{ class?: HTMLAttributes['class'] }>();
|
|
21
|
+
const select = injectReactiveOrFail<SelectExpose>('select', '<SelectTrigger> must be a child of a <Select>');
|
|
22
|
+
const renderedClasses = computed(() =>
|
|
23
|
+
classes(
|
|
24
|
+
// eslint-disable-next-line vue/max-len
|
|
25
|
+
'focus:outline-primary-600 data-[state=open]:outline-primary-600 grid w-full cursor-default grid-cols-1 rounded-md bg-white py-1.5 pr-2 pl-3 text-left text-gray-900 outline-1 -outline-offset-1 outline-gray-300 focus:outline-2 focus:-outline-offset-2 sm:text-sm/6',
|
|
26
|
+
{ 'mt-1': select.label },
|
|
27
|
+
rootClasses,
|
|
28
|
+
));
|
|
29
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Modal :title="$td('settings.title', 'Settings')">
|
|
3
|
+
<component :is="setting.component" v-for="(setting, key) in settings" :key />
|
|
4
|
+
</Modal>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { arraySorted } from '@noeldemartin/utils';
|
|
9
|
+
import { computed } from 'vue';
|
|
10
|
+
|
|
11
|
+
import App from '@aerogel/core/services/App';
|
|
12
|
+
import Modal from '@aerogel/core/components/ui/Modal.vue';
|
|
13
|
+
|
|
14
|
+
const settings = computed(() => arraySorted(App.settings, 'priority', 'desc'));
|
|
15
|
+
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="grid grow place-items-center">
|
|
3
|
+
<div class="flex flex-col items-center space-y-6 p-8">
|
|
4
|
+
<h1 class="mt-2 text-center text-4xl font-medium text-red-600">
|
|
5
|
+
{{ $td('startupCrash.title', 'Something went wrong!') }}
|
|
6
|
+
</h1>
|
|
7
|
+
<Markdown
|
|
8
|
+
:text="
|
|
9
|
+
$td(
|
|
10
|
+
'startupCrash.message',
|
|
11
|
+
'Something failed trying to start the application.\n\nHere\'s some things you can do:'
|
|
12
|
+
)
|
|
13
|
+
"
|
|
14
|
+
class="mt-4 text-center"
|
|
15
|
+
/>
|
|
16
|
+
<div class="mt-4 flex flex-col space-y-4">
|
|
17
|
+
<Button variant="danger" @click="$app.reload()">
|
|
18
|
+
{{ $td('startupCrash.reload', 'Try again') }}
|
|
19
|
+
</Button>
|
|
20
|
+
<Button variant="danger" @click="$errors.inspect($errors.startupErrors)">
|
|
21
|
+
{{ $td('startupCrash.inspect', 'View error details') }}
|
|
22
|
+
</Button>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup lang="ts">
|
|
29
|
+
import Markdown from '@aerogel/core/components/ui/Markdown.vue';
|
|
30
|
+
import Button from '@aerogel/core/components/ui/Button.vue';
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<HeadlessToast :class="renderedClasses">
|
|
3
|
+
<Markdown v-if="message" :text="message" inline />
|
|
4
|
+
|
|
5
|
+
<Button
|
|
6
|
+
v-for="(action, key) of actions"
|
|
7
|
+
:key
|
|
8
|
+
:action
|
|
9
|
+
:variant
|
|
10
|
+
:as="HeadlessToastAction"
|
|
11
|
+
/>
|
|
12
|
+
</HeadlessToast>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import type { HTMLAttributes } from 'vue';
|
|
17
|
+
|
|
18
|
+
import Button from '@aerogel/core/components/ui/Button.vue';
|
|
19
|
+
import Markdown from '@aerogel/core/components/ui/Markdown.vue';
|
|
20
|
+
import HeadlessToast from '@aerogel/core/components/headless/HeadlessToast.vue';
|
|
21
|
+
import HeadlessToastAction from '@aerogel/core/components/headless/HeadlessToastAction.vue';
|
|
22
|
+
import { computedVariantClasses } from '@aerogel/core/utils/classes';
|
|
23
|
+
import type { ToastExpose, ToastProps } from '@aerogel/core/components/contracts/Toast';
|
|
24
|
+
import type { Variants } from '@aerogel/core/utils/classes';
|
|
25
|
+
|
|
26
|
+
const { class: baseClasses, variant = 'secondary' } = defineProps<ToastProps & { class?: HTMLAttributes['class'] }>();
|
|
27
|
+
const renderedClasses = computedVariantClasses<Variants<Pick<ToastProps, 'variant'>>>(
|
|
28
|
+
{ baseClasses, variant },
|
|
29
|
+
{
|
|
30
|
+
baseClasses: 'flex items-center gap-2 rounded-md p-2 ring-1 shadow-lg border-gray-200',
|
|
31
|
+
variants: {
|
|
32
|
+
variant: {
|
|
33
|
+
secondary: 'bg-gray-900 text-white ring-black',
|
|
34
|
+
danger: 'bg-red-50 text-red-900 ring-red-100',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
defaultVariants: {
|
|
38
|
+
variant: 'secondary',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
defineExpose<ToastExpose>();
|
|
44
|
+
</script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export { default as AdvancedOptions } from './AdvancedOptions.vue';
|
|
2
|
+
export { default as AlertModal } from './AlertModal.vue';
|
|
3
|
+
export { default as Button } from './Button.vue';
|
|
4
|
+
export { default as Checkbox } from './Checkbox.vue';
|
|
5
|
+
export { default as ConfirmModal } from './ConfirmModal.vue';
|
|
6
|
+
export { default as DropdownMenu } from './DropdownMenu.vue';
|
|
7
|
+
export { default as DropdownMenuOption } from './DropdownMenuOption.vue';
|
|
8
|
+
export { default as DropdownMenuOptions } from './DropdownMenuOptions.vue';
|
|
9
|
+
export { default as EditableContent } from './EditableContent.vue';
|
|
10
|
+
export { default as ErrorMessage } from './ErrorMessage.vue';
|
|
11
|
+
export { default as ErrorReportModal } from './ErrorReportModal.vue';
|
|
12
|
+
export { default as ErrorReportModalButtons } from './ErrorReportModalButtons.vue';
|
|
13
|
+
export { default as ErrorReportModalTitle } from './ErrorReportModalTitle.vue';
|
|
14
|
+
export { default as Form } from './Form.vue';
|
|
15
|
+
export { default as Input } from './Input.vue';
|
|
16
|
+
export { default as Link } from './Link.vue';
|
|
17
|
+
export { default as LoadingModal } from './LoadingModal.vue';
|
|
18
|
+
export { default as Markdown } from './Markdown.vue';
|
|
19
|
+
export { default as Modal } from './Modal.vue';
|
|
20
|
+
export { default as ModalContext } from './ModalContext.vue';
|
|
21
|
+
export { default as ProgressBar } from './ProgressBar.vue';
|
|
22
|
+
export { default as PromptModal } from './PromptModal.vue';
|
|
23
|
+
export { default as Select } from './Select.vue';
|
|
24
|
+
export { default as SelectLabel } from './SelectLabel.vue';
|
|
25
|
+
export { default as SelectOption } from './SelectOption.vue';
|
|
26
|
+
export { default as SelectOptions } from './SelectOptions.vue';
|
|
27
|
+
export { default as SelectTrigger } from './SelectTrigger.vue';
|
|
28
|
+
export { default as SettingsModal } from './SettingsModal.vue';
|
|
29
|
+
export { default as StartupCrash } from './StartupCrash.vue';
|
|
30
|
+
export { default as Toast } from './Toast.vue';
|
package/src/directives/index.ts
CHANGED
|
@@ -1,15 +1,36 @@
|
|
|
1
1
|
import type { Directive } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { definePlugin } from '
|
|
3
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import measure from './measure';
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const builtInDirectives: Record<string, Directive> = {
|
|
8
|
+
measure: measure,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
export * from './measure';
|
|
12
|
+
|
|
11
13
|
export default definePlugin({
|
|
12
|
-
install(app) {
|
|
13
|
-
|
|
14
|
+
install(app, options) {
|
|
15
|
+
const directives = {
|
|
16
|
+
...builtInDirectives,
|
|
17
|
+
...options.directives,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
for (const [name, directive] of Object.entries(directives)) {
|
|
21
|
+
app.directive(name, directive);
|
|
22
|
+
}
|
|
14
23
|
},
|
|
15
24
|
});
|
|
25
|
+
|
|
26
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
27
|
+
export interface AerogelOptions {
|
|
28
|
+
directives?: Record<string, Directive>;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
declare module 'vue' {
|
|
33
|
+
interface ComponentCustomDirectives {
|
|
34
|
+
measure: Directive<string, string>;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineDirective } from '@aerogel/core/utils/vue';
|
|
2
|
+
import { tap } from '@noeldemartin/utils';
|
|
3
|
+
|
|
4
|
+
const resizeObservers: WeakMap<HTMLElement, ResizeObserver> = new WeakMap();
|
|
5
|
+
|
|
6
|
+
export interface ElementSize {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type MeasureDirectiveListener = (size: ElementSize) => unknown;
|
|
12
|
+
|
|
13
|
+
export default defineDirective({
|
|
14
|
+
mounted(element: HTMLElement, { value }) {
|
|
15
|
+
// TODO replace with argument when typed properly
|
|
16
|
+
const modifiers = { css: true, watch: true };
|
|
17
|
+
|
|
18
|
+
const listener = typeof value === 'function' ? (value as MeasureDirectiveListener) : null;
|
|
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
|
+
}
|
|
26
|
+
|
|
27
|
+
listener?.({ width: sizes.width, height: sizes.height });
|
|
28
|
+
};
|
|
29
|
+
|
|
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);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { JSError } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
import { defineServiceState } from '@aerogel/core/services';
|
|
4
|
+
|
|
5
|
+
export type ErrorSource = string | Error | JSError | unknown;
|
|
6
|
+
|
|
7
|
+
export interface ErrorReport {
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
details?: string;
|
|
11
|
+
error?: Error | JSError | unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ErrorReportLog {
|
|
15
|
+
report: ErrorReport;
|
|
16
|
+
seen: boolean;
|
|
17
|
+
date: Date;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default defineServiceState({
|
|
21
|
+
name: 'errors',
|
|
22
|
+
initialState: {
|
|
23
|
+
logs: [] as ErrorReportLog[],
|
|
24
|
+
startupErrors: [] as ErrorReport[],
|
|
25
|
+
},
|
|
26
|
+
computed: {
|
|
27
|
+
hasErrors: ({ logs }) => logs.length > 0,
|
|
28
|
+
hasNewErrors: ({ logs }) => logs.some((error) => !error.seen),
|
|
29
|
+
hasStartupErrors: ({ startupErrors }) => startupErrors.length > 0,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { JSError, facade, isDevelopment, isObject, isTesting, objectWithoutEmpty, toString } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
import App from '@aerogel/core/services/App';
|
|
4
|
+
import ServiceBootError from '@aerogel/core/errors/ServiceBootError';
|
|
5
|
+
import UI from '@aerogel/core/ui/UI';
|
|
6
|
+
import { translateWithDefault } from '@aerogel/core/lang/utils';
|
|
7
|
+
import { Events } from '@aerogel/core/services';
|
|
8
|
+
|
|
9
|
+
import Service from './Errors.state';
|
|
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 as ErrorReport[]) : [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.modal(UI.requireComponent('error-report-modal'), { reports });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public async report(error: ErrorSource, message?: string): Promise<void> {
|
|
38
|
+
await Events.emit('error', { error, message });
|
|
39
|
+
|
|
40
|
+
if (isTesting('unit')) {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (isDevelopment()) {
|
|
45
|
+
this.logError(error);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!this.enabled) {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!App.isMounted()) {
|
|
53
|
+
const startupError = await this.createStartupErrorReport(error);
|
|
54
|
+
|
|
55
|
+
if (startupError) {
|
|
56
|
+
this.setState({ startupErrors: this.startupErrors.concat(startupError) });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const report = await this.createErrorReport(error);
|
|
63
|
+
const log: ErrorReportLog = {
|
|
64
|
+
report,
|
|
65
|
+
seen: false,
|
|
66
|
+
date: new Date(),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
UI.toast(
|
|
70
|
+
message ??
|
|
71
|
+
translateWithDefault('errors.notice', 'Something went wrong, but it\'s not your fault. Try again!'),
|
|
72
|
+
{
|
|
73
|
+
variant: 'danger',
|
|
74
|
+
actions: [
|
|
75
|
+
{
|
|
76
|
+
label: translateWithDefault('errors.viewDetails', 'View details'),
|
|
77
|
+
dismiss: true,
|
|
78
|
+
click: () => UI.modal(UI.requireComponent('error-report-modal'), { reports: [report] }),
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
this.setState({ logs: [log].concat(this.logs) });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public see(report: ErrorReport): void {
|
|
88
|
+
this.setState({
|
|
89
|
+
logs: this.logs.map((log) => {
|
|
90
|
+
if (log.report !== report) {
|
|
91
|
+
return log;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
...log,
|
|
96
|
+
seen: true,
|
|
97
|
+
};
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public seeAll(): void {
|
|
103
|
+
this.setState({
|
|
104
|
+
logs: this.logs.map((log) => ({
|
|
105
|
+
...log,
|
|
106
|
+
seen: true,
|
|
107
|
+
})),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private logError(error: unknown): void {
|
|
112
|
+
// eslint-disable-next-line no-console
|
|
113
|
+
console.error(error);
|
|
114
|
+
|
|
115
|
+
if (isObject(error) && error.cause) {
|
|
116
|
+
this.logError(error.cause);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async createErrorReport(error: ErrorSource): Promise<ErrorReport> {
|
|
121
|
+
if (typeof error === 'string') {
|
|
122
|
+
return { title: error };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (error instanceof Error || error instanceof JSError) {
|
|
126
|
+
return this.createErrorReportFromError(error);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (isObject(error)) {
|
|
130
|
+
return objectWithoutEmpty({
|
|
131
|
+
title: toString(
|
|
132
|
+
error['name'] ?? error['title'] ?? translateWithDefault('errors.unknown', 'Unknown Error'),
|
|
133
|
+
),
|
|
134
|
+
description: toString(
|
|
135
|
+
error['message'] ??
|
|
136
|
+
error['description'] ??
|
|
137
|
+
translateWithDefault('errors.unknownDescription', 'Unknown error object'),
|
|
138
|
+
),
|
|
139
|
+
error,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
title: translateWithDefault('errors.unknown', 'Unknown Error'),
|
|
145
|
+
error,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async createStartupErrorReport(error: ErrorSource): Promise<ErrorReport | null> {
|
|
150
|
+
if (error instanceof ServiceBootError) {
|
|
151
|
+
// Ignore second-order boot errors in order to have a cleaner startup crash screen.
|
|
152
|
+
return error.cause instanceof ServiceBootError ? null : this.createErrorReport(error.cause);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return this.createErrorReport(error);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private createErrorReportFromError(error: Error | JSError, defaults: Partial<ErrorReport> = {}): ErrorReport {
|
|
159
|
+
return {
|
|
160
|
+
title: error.name,
|
|
161
|
+
description: error.message,
|
|
162
|
+
details: error.stack,
|
|
163
|
+
error,
|
|
164
|
+
...defaults,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default facade(ErrorsService);
|
|
171
|
+
|
|
172
|
+
declare module '@aerogel/core/services/Events' {
|
|
173
|
+
export interface EventsPayload {
|
|
174
|
+
error: { error: ErrorSource; message?: string };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { App } from 'vue';
|
|
2
|
+
|
|
3
|
+
import { bootServices } from '@aerogel/core/services';
|
|
4
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
5
|
+
|
|
6
|
+
import Errors from './Errors';
|
|
7
|
+
import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
8
|
+
|
|
9
|
+
export * from './utils';
|
|
10
|
+
export { Errors };
|
|
11
|
+
export { default as JobCancelledError } from './JobCancelledError';
|
|
12
|
+
export { default as ServiceBootError } from './ServiceBootError';
|
|
13
|
+
export type { ErrorSource, ErrorReport, ErrorReportLog };
|
|
14
|
+
|
|
15
|
+
const services = { $errors: Errors };
|
|
16
|
+
const frameworkHandler: ErrorHandler = (error) => {
|
|
17
|
+
Errors.report(error);
|
|
18
|
+
|
|
19
|
+
return true;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function setUpErrorHandler(app: App, baseHandler: ErrorHandler = () => false): void {
|
|
23
|
+
const errorHandler: ErrorHandler = (error) => baseHandler(error) || frameworkHandler(error);
|
|
24
|
+
|
|
25
|
+
app.config.errorHandler = errorHandler;
|
|
26
|
+
globalThis.onerror = (event, _, __, ___, error) => errorHandler(error ?? event);
|
|
27
|
+
globalThis.onunhandledrejection = (event) => errorHandler(event.reason);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ErrorHandler = (error: ErrorSource) => boolean;
|
|
31
|
+
export type ErrorsServices = typeof services;
|
|
32
|
+
|
|
33
|
+
export default definePlugin({
|
|
34
|
+
async install(app, options) {
|
|
35
|
+
setUpErrorHandler(app, options.handleError);
|
|
36
|
+
|
|
37
|
+
await bootServices(app, services);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
42
|
+
export interface AerogelOptions {
|
|
43
|
+
handleError?(error: ErrorSource): boolean;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare module '@aerogel/core/services' {
|
|
48
|
+
export interface Services extends ErrorsServices {}
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { JSError, isObject, toString } from '@noeldemartin/utils';
|
|
2
|
+
import { translateWithDefault } from '@aerogel/core/lang/utils';
|
|
3
|
+
import type { ErrorSource } from './Errors.state';
|
|
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
|
+
|
|
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
|
+
|
|
22
|
+
if (typeof error === 'string') {
|
|
23
|
+
return error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (error instanceof Error || error instanceof JSError) {
|
|
27
|
+
return error.message;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (isObject(error)) {
|
|
31
|
+
return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return translateWithDefault('errors.unknown', 'Unknown Error');
|
|
35
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
2
|
+
import { tt } from '@noeldemartin/testing';
|
|
3
|
+
import type { Equals } from '@noeldemartin/utils';
|
|
4
|
+
import type { Expect } from '@noeldemartin/testing';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
numberInput,
|
|
8
|
+
objectInput,
|
|
9
|
+
requiredObjectInput,
|
|
10
|
+
requiredStringInput,
|
|
11
|
+
stringInput,
|
|
12
|
+
} from '@aerogel/core/forms/utils';
|
|
13
|
+
import { useForm } from '@aerogel/core/utils/composition/forms';
|
|
14
|
+
|
|
15
|
+
describe('FormController', () => {
|
|
16
|
+
|
|
17
|
+
it('defines magic fields', () => {
|
|
18
|
+
const form = useForm({
|
|
19
|
+
name: requiredStringInput(),
|
|
20
|
+
age: numberInput(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expectTypeOf(form.name).toEqualTypeOf<string>();
|
|
24
|
+
expectTypeOf(form.age).toEqualTypeOf<number | null>();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('validates required fields', () => {
|
|
28
|
+
// Arrange
|
|
29
|
+
const form = useForm({
|
|
30
|
+
name: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
rules: 'required',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
form.submit();
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(form.valid).toBe(false);
|
|
41
|
+
expect(form.submitted).toBe(true);
|
|
42
|
+
expect(form.errors.name).toEqual(['required']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('resets form', () => {
|
|
46
|
+
// Arrange
|
|
47
|
+
const form = useForm({
|
|
48
|
+
name: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
rules: 'required',
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
form.name = 'Foo bar';
|
|
55
|
+
form.submit();
|
|
56
|
+
|
|
57
|
+
// Act
|
|
58
|
+
form.reset();
|
|
59
|
+
|
|
60
|
+
// Assert
|
|
61
|
+
expect(form.valid).toBe(true);
|
|
62
|
+
expect(form.submitted).toBe(false);
|
|
63
|
+
expect(form.name).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('trims values', () => {
|
|
67
|
+
// Arrange
|
|
68
|
+
const form = useForm({
|
|
69
|
+
trimmed: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
rules: 'required',
|
|
72
|
+
},
|
|
73
|
+
untrimmed: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
rules: 'required',
|
|
76
|
+
trim: false,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Act
|
|
81
|
+
form.trimmed = ' ';
|
|
82
|
+
form.untrimmed = ' ';
|
|
83
|
+
|
|
84
|
+
form.submit();
|
|
85
|
+
|
|
86
|
+
// Assert
|
|
87
|
+
expect(form.valid).toBe(false);
|
|
88
|
+
expect(form.submitted).toBe(true);
|
|
89
|
+
expect(form.trimmed).toEqual('');
|
|
90
|
+
expect(form.untrimmed).toEqual(' ');
|
|
91
|
+
expect(form.errors).toEqual({ trimmed: ['required'], untrimmed: null });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('infers field types', () => {
|
|
95
|
+
const form = useForm({
|
|
96
|
+
one: stringInput(),
|
|
97
|
+
two: requiredStringInput(),
|
|
98
|
+
three: objectInput(),
|
|
99
|
+
four: requiredObjectInput<{ foo: string; bar?: number }>(),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
tt<
|
|
103
|
+
| Expect<Equals<typeof form.one, string | null>>
|
|
104
|
+
| Expect<Equals<typeof form.two, string>>
|
|
105
|
+
| Expect<Equals<typeof form.three, object | null>>
|
|
106
|
+
| Expect<Equals<typeof form.four, { foo: string; bar?: number }>>
|
|
107
|
+
>();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
});
|