@aerogel/core 0.0.0-next.7f6ed5a1f91688a86bf5ede2adc465e4fd6cfdea → 0.0.0-next.8323c60b905020dcb3bd9d4b0bc8d9b6529e1082
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.css +1 -0
- package/dist/aerogel-core.d.ts +2547 -422
- package/dist/aerogel-core.js +3722 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +39 -37
- package/src/bootstrap/bootstrap.test.ts +7 -66
- package/src/bootstrap/index.ts +46 -33
- package/src/bootstrap/options.ts +8 -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 +19 -0
- package/src/components/contracts/Button.ts +16 -0
- package/src/components/contracts/ConfirmModal.ts +48 -0
- package/src/components/contracts/DropdownMenu.ts +25 -0
- package/src/components/contracts/ErrorReportModal.ts +33 -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/HeadlessInputError.vue +22 -0
- package/src/components/headless/HeadlessInputInput.vue +86 -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/HeadlessSwitch.vue +96 -0
- package/src/components/headless/HeadlessToast.vue +18 -0
- package/src/components/headless/HeadlessToastAction.vue +13 -0
- package/src/components/headless/index.ts +20 -2
- package/src/components/index.ts +6 -6
- package/src/components/ui/AdvancedOptions.vue +18 -0
- package/src/components/ui/AlertModal.vue +17 -0
- package/src/components/ui/Button.vue +115 -0
- package/src/components/ui/Checkbox.vue +56 -0
- package/src/components/ui/ConfirmModal.vue +50 -0
- package/src/components/ui/DropdownMenu.vue +32 -0
- package/src/components/ui/DropdownMenuOption.vue +22 -0
- package/src/components/ui/DropdownMenuOptions.vue +44 -0
- package/src/components/ui/EditableContent.vue +82 -0
- package/src/components/ui/ErrorLogs.vue +19 -0
- package/src/components/ui/ErrorLogsModal.vue +48 -0
- package/src/components/ui/ErrorMessage.vue +15 -0
- package/src/components/ui/ErrorReportModal.vue +73 -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 +97 -0
- package/src/components/ui/Modal.vue +131 -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 +21 -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/Setting.vue +31 -0
- package/src/components/ui/SettingsModal.vue +15 -0
- package/src/components/ui/StartupCrash.vue +31 -0
- package/src/components/ui/Switch.vue +11 -0
- package/src/components/ui/TextArea.vue +56 -0
- package/src/components/ui/Toast.vue +46 -0
- package/src/components/ui/index.ts +35 -0
- package/src/directives/index.ts +29 -6
- package/src/directives/measure.ts +46 -0
- package/src/errors/Errors.state.ts +31 -0
- package/src/errors/Errors.ts +200 -0
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +53 -0
- package/src/errors/settings/Debug.vue +32 -0
- package/src/errors/settings/index.ts +10 -0
- package/src/errors/utils.ts +35 -0
- package/src/forms/FormController.test.ts +113 -0
- package/src/forms/FormController.ts +255 -0
- package/src/forms/index.ts +3 -2
- package/src/forms/utils.ts +87 -14
- package/src/forms/validation.ts +50 -0
- package/src/index.css +76 -0
- package/src/{main.ts → index.ts} +5 -0
- 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 +63 -9
- package/src/lang/index.ts +22 -75
- 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 +8 -0
- package/src/plugins/index.ts +29 -0
- package/src/services/App.state.ts +50 -0
- package/src/services/App.ts +63 -0
- 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 +273 -35
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +45 -8
- package/src/services/store.ts +30 -0
- package/src/services/utils.ts +18 -0
- package/src/testing/index.ts +30 -0
- package/src/testing/setup.ts +11 -0
- package/src/types/vite.d.ts +0 -2
- package/src/ui/UI.state.ts +21 -13
- package/src/ui/UI.ts +350 -53
- package/src/ui/index.ts +40 -25
- package/src/ui/utils.ts +16 -0
- package/src/utils/app.ts +7 -0
- package/src/utils/classes.ts +41 -0
- package/src/utils/composition/events.ts +4 -5
- package/src/utils/composition/forms.ts +27 -0
- package/src/utils/composition/hooks.ts +9 -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 +9 -0
- package/src/utils/markdown.test.ts +50 -0
- package/src/utils/markdown.ts +60 -4
- package/src/utils/types.ts +3 -0
- package/src/utils/vue.ts +38 -121
- 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/bootstrap/hooks.ts +0 -19
- package/src/components/AGAppLayout.vue +0 -11
- package/src/components/AGAppOverlays.vue +0 -39
- package/src/components/basic/AGMarkdown.vue +0 -20
- package/src/components/basic/index.ts +0 -3
- package/src/components/forms/AGButton.vue +0 -11
- 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/AGHeadlessInputError.vue +0 -22
- package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -29
- package/src/components/headless/forms/index.ts +0 -4
- package/src/components/headless/modals/AGHeadlessModal.ts +0 -7
- package/src/components/headless/modals/AGHeadlessModal.vue +0 -84
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -20
- 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 -15
- package/src/components/modals/AGModal.ts +0 -6
- 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 -5
- package/src/directives/initial-focus.ts +0 -11
- package/src/forms/Form.test.ts +0 -37
- package/src/forms/Form.ts +0 -154
- package/src/forms/composition.ts +0 -6
- package/src/lang/helpers.ts +0 -5
- package/src/models/index.ts +0 -18
- package/src/routing/index.ts +0 -33
- package/src/testing/stubs/lang/en.yaml +0 -1
- package/src/testing/stubs/models/User.ts +0 -3
- package/tsconfig.json +0 -19
- package/vite.config.ts +0 -17
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { tap } from '@noeldemartin/utils';
|
|
2
|
+
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
|
3
|
+
import type { DefineStoreOptions, Pinia, StateTree, Store, _GettersTree } from 'pinia';
|
|
4
|
+
|
|
5
|
+
let _store: Pinia | null = null;
|
|
6
|
+
|
|
7
|
+
function initializePiniaStore(): Pinia {
|
|
8
|
+
return _store ?? resetPiniaStore();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resetPiniaStore(): Pinia {
|
|
12
|
+
return tap(createPinia(), (store) => {
|
|
13
|
+
_store = store;
|
|
14
|
+
|
|
15
|
+
setActivePinia(store);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getPiniaStore(): Pinia {
|
|
20
|
+
return _store ?? initializePiniaStore();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function defineServiceStore<Id extends string, S extends StateTree = {}, G extends _GettersTree<S> = {}, A = {}>(
|
|
24
|
+
name: Id,
|
|
25
|
+
options: Omit<DefineStoreOptions<Id, S, G, A>, 'id'>,
|
|
26
|
+
): Store<Id, S, G, A> {
|
|
27
|
+
initializePiniaStore();
|
|
28
|
+
|
|
29
|
+
return defineStore(name, options)();
|
|
30
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { objectOnly } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
export type Replace<
|
|
4
|
+
TOriginal extends Record<string, unknown>,
|
|
5
|
+
TReplacements extends Partial<Record<keyof TOriginal, unknown>>,
|
|
6
|
+
> = {
|
|
7
|
+
[K in keyof TOriginal]: TReplacements extends Record<K, infer Replacement> ? Replacement : TOriginal[K];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function replaceExisting<
|
|
11
|
+
TOriginal extends Record<string, unknown>,
|
|
12
|
+
TReplacements extends Partial<Record<keyof TOriginal, unknown>>,
|
|
13
|
+
>(original: TOriginal, replacements: TReplacements): Replace<TOriginal, TReplacements> {
|
|
14
|
+
return {
|
|
15
|
+
...original,
|
|
16
|
+
...objectOnly(replacements, Object.keys(original)),
|
|
17
|
+
} as Replace<TOriginal, TReplacements>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isTesting } from '@noeldemartin/utils';
|
|
2
|
+
import type { GetClosureArgs } from '@noeldemartin/utils';
|
|
3
|
+
|
|
4
|
+
import Events from '@aerogel/core/services/Events';
|
|
5
|
+
import { App } from '@aerogel/core/services';
|
|
6
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
7
|
+
import type { Services } from '@aerogel/core/services';
|
|
8
|
+
|
|
9
|
+
export interface AerogelTestingRuntime {
|
|
10
|
+
on: (typeof Events)['on'];
|
|
11
|
+
service<T extends keyof Services>(name: T): Services[T] | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default definePlugin({
|
|
15
|
+
async install() {
|
|
16
|
+
if (!isTesting()) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
globalThis.testingRuntime = {
|
|
21
|
+
on: ((...args: GetClosureArgs<(typeof Events)['on']>) => Events.on(...args)) as (typeof Events)['on'],
|
|
22
|
+
service: (name) => App.service(name),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
declare global {
|
|
28
|
+
// eslint-disable-next-line no-var
|
|
29
|
+
var testingRuntime: AerogelTestingRuntime | undefined;
|
|
30
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FakeLocalStorage } from '@noeldemartin/testing';
|
|
2
|
+
import { beforeEach, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
vi.mock('dompurify', async () => {
|
|
5
|
+
return { default: { sanitize: (html: string) => html } };
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
FakeLocalStorage.reset();
|
|
10
|
+
FakeLocalStorage.patchGlobal();
|
|
11
|
+
});
|
package/src/types/vite.d.ts
CHANGED
package/src/ui/UI.state.ts
CHANGED
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
import type { Component } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { defineServiceState } from '
|
|
3
|
+
import { defineServiceState } from '@aerogel/core/services/Service';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
modals: Modal[];
|
|
7
|
-
}
|
|
5
|
+
import { Layouts, getCurrentLayout } from './utils';
|
|
8
6
|
|
|
9
|
-
export interface
|
|
7
|
+
export interface UIModal<T = unknown> {
|
|
10
8
|
id: string;
|
|
11
9
|
properties: Record<string, unknown>;
|
|
12
10
|
component: Component;
|
|
11
|
+
closing: boolean;
|
|
13
12
|
beforeClose: Promise<T | undefined>;
|
|
14
13
|
afterClose: Promise<T | undefined>;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
export interface
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
> {}
|
|
16
|
+
export interface UIToast {
|
|
17
|
+
id: string;
|
|
18
|
+
component: Component;
|
|
19
|
+
properties: Record<string, unknown>;
|
|
20
|
+
}
|
|
23
21
|
|
|
24
|
-
export default defineServiceState
|
|
25
|
-
|
|
22
|
+
export default defineServiceState({
|
|
23
|
+
name: 'ui',
|
|
24
|
+
initialState: {
|
|
25
|
+
modals: [] as UIModal[],
|
|
26
|
+
toasts: [] as UIToast[],
|
|
27
|
+
layout: getCurrentLayout(),
|
|
28
|
+
},
|
|
29
|
+
computed: {
|
|
30
|
+
desktop: ({ layout }) => layout === Layouts.Desktop,
|
|
31
|
+
mobile: ({ layout }) => layout === Layouts.Mobile,
|
|
32
|
+
openModals: ({ modals }) => modals.filter(({ closing }) => !closing),
|
|
33
|
+
},
|
|
26
34
|
});
|
package/src/ui/UI.ts
CHANGED
|
@@ -1,56 +1,299 @@
|
|
|
1
|
-
import { facade, fail, uuid } from '@noeldemartin/utils';
|
|
2
|
-
import { markRaw, nextTick } from 'vue';
|
|
1
|
+
import { after, facade, fail, isDevelopment, required, uuid } from '@noeldemartin/utils';
|
|
2
|
+
import { markRaw, nextTick, unref } from 'vue';
|
|
3
|
+
import type { ComponentExposed, ComponentProps } from 'vue-component-type-helpers';
|
|
3
4
|
import type { Component } from 'vue';
|
|
4
|
-
import type {
|
|
5
|
+
import type { ClosureArgs } from '@noeldemartin/utils';
|
|
5
6
|
|
|
6
|
-
import
|
|
7
|
+
import App from '@aerogel/core/services/App';
|
|
8
|
+
import Events from '@aerogel/core/services/Events';
|
|
9
|
+
import type {
|
|
10
|
+
ConfirmModalCheckboxes,
|
|
11
|
+
ConfirmModalExpose,
|
|
12
|
+
ConfirmModalProps,
|
|
13
|
+
} from '@aerogel/core/components/contracts/ConfirmModal';
|
|
14
|
+
import type {
|
|
15
|
+
ErrorReportModalExpose,
|
|
16
|
+
ErrorReportModalProps,
|
|
17
|
+
} from '@aerogel/core/components/contracts/ErrorReportModal';
|
|
18
|
+
import type { AcceptRefs } from '@aerogel/core/utils';
|
|
19
|
+
import type { AlertModalExpose, AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
|
|
20
|
+
import type { ButtonVariant } from '@aerogel/core/components/contracts/Button';
|
|
21
|
+
import type { LoadingModalExpose, LoadingModalProps } from '@aerogel/core/components/contracts/LoadingModal';
|
|
22
|
+
import type { PromptModalExpose, PromptModalProps } from '@aerogel/core/components/contracts/PromptModal';
|
|
23
|
+
import type { ToastAction, ToastExpose, ToastProps, ToastVariant } from '@aerogel/core/components/contracts/Toast';
|
|
7
24
|
|
|
8
25
|
import Service from './UI.state';
|
|
9
|
-
import
|
|
26
|
+
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
27
|
+
import type { UIModal, UIToast } from './UI.state';
|
|
10
28
|
|
|
11
29
|
interface ModalCallbacks<T = unknown> {
|
|
12
30
|
willClose(result: T | undefined): void;
|
|
13
|
-
|
|
31
|
+
hasClosed(result: T | undefined): void;
|
|
14
32
|
}
|
|
15
33
|
|
|
16
|
-
type
|
|
17
|
-
type
|
|
18
|
-
|
|
19
|
-
: never;
|
|
34
|
+
export type ModalResult<T> = ModalExposeResult<ComponentExposed<T>>;
|
|
35
|
+
export type ModalExposeResult<T> = T extends { close(result?: infer Result): Promise<void> } ? Result : unknown;
|
|
36
|
+
export type UIComponent<Props = {}, Exposed = {}> = { new (...args: ClosureArgs): Exposed & { $props: Props } };
|
|
20
37
|
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
38
|
+
export interface UIComponents {
|
|
39
|
+
'alert-modal': UIComponent<AlertModalProps, AlertModalExpose>;
|
|
40
|
+
'confirm-modal': UIComponent<ConfirmModalProps, ConfirmModalExpose>;
|
|
41
|
+
'error-report-modal': UIComponent<ErrorReportModalProps, ErrorReportModalExpose>;
|
|
42
|
+
'loading-modal': UIComponent<LoadingModalProps, LoadingModalExpose>;
|
|
43
|
+
'prompt-modal': UIComponent<PromptModalProps, PromptModalExpose>;
|
|
44
|
+
'router-link': UIComponent;
|
|
45
|
+
'startup-crash': UIComponent;
|
|
46
|
+
toast: UIComponent<ToastProps, ToastExpose>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface UIModalContext {
|
|
50
|
+
modal: UIModal;
|
|
51
|
+
childIndex?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ConfirmOptions = AcceptRefs<{
|
|
55
|
+
acceptText?: string;
|
|
56
|
+
acceptVariant?: ButtonVariant;
|
|
57
|
+
cancelText?: string;
|
|
58
|
+
cancelVariant?: ButtonVariant;
|
|
59
|
+
actions?: Record<string, () => unknown>;
|
|
60
|
+
required?: boolean;
|
|
61
|
+
}>;
|
|
62
|
+
|
|
63
|
+
export type LoadingOptions = AcceptRefs<{
|
|
64
|
+
title?: string;
|
|
65
|
+
message?: string;
|
|
66
|
+
progress?: number;
|
|
67
|
+
delay?: number;
|
|
68
|
+
}>;
|
|
24
69
|
|
|
25
|
-
export
|
|
70
|
+
export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
|
|
71
|
+
extends ConfirmOptions {
|
|
72
|
+
checkboxes?: T;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type PromptOptions = AcceptRefs<{
|
|
76
|
+
label?: string;
|
|
77
|
+
defaultValue?: string;
|
|
78
|
+
placeholder?: string;
|
|
79
|
+
acceptText?: string;
|
|
80
|
+
acceptVariant?: ButtonVariant;
|
|
81
|
+
cancelText?: string;
|
|
82
|
+
cancelVariant?: ButtonVariant;
|
|
83
|
+
trim?: boolean;
|
|
84
|
+
}>;
|
|
85
|
+
|
|
86
|
+
export interface ToastOptions {
|
|
87
|
+
component?: Component;
|
|
88
|
+
variant?: ToastVariant;
|
|
89
|
+
actions?: ToastAction[];
|
|
90
|
+
}
|
|
26
91
|
|
|
27
92
|
export class UIService extends Service {
|
|
28
93
|
|
|
29
94
|
private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
|
|
30
|
-
private components: Partial<
|
|
95
|
+
private components: Partial<UIComponents> = {};
|
|
96
|
+
|
|
97
|
+
public registerComponent<T extends keyof UIComponents>(name: T, component: UIComponents[T]): void {
|
|
98
|
+
this.components[name] = component;
|
|
99
|
+
}
|
|
31
100
|
|
|
32
|
-
public
|
|
33
|
-
this.
|
|
101
|
+
public resolveComponent<T extends keyof UIComponents>(name: T): UIComponents[T] | null {
|
|
102
|
+
return this.components[name] ?? null;
|
|
34
103
|
}
|
|
35
104
|
|
|
36
|
-
public
|
|
37
|
-
this.
|
|
105
|
+
public requireComponent<T extends keyof UIComponents>(name: T): UIComponents[T] {
|
|
106
|
+
return this.resolveComponent(name) ?? fail(`UI Component '${name}' is not defined!`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public alert(message: string): void;
|
|
110
|
+
public alert(title: string, message: string): void;
|
|
111
|
+
public alert(messageOrTitle: string, message?: string): void {
|
|
112
|
+
const getProperties = (): AlertModalProps => {
|
|
113
|
+
if (typeof message !== 'string') {
|
|
114
|
+
return { message: messageOrTitle };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
title: messageOrTitle,
|
|
119
|
+
message,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
this.modal(this.requireComponent('alert-modal'), getProperties());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* eslint-disable max-len */
|
|
127
|
+
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
128
|
+
public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
129
|
+
public async confirm<T extends ConfirmModalCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
130
|
+
public async confirm<T extends ConfirmModalCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
131
|
+
/* eslint-enable max-len */
|
|
132
|
+
|
|
133
|
+
public async confirm(
|
|
134
|
+
messageOrTitle: string,
|
|
135
|
+
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
136
|
+
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
137
|
+
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
138
|
+
const getProperties = (): AcceptRefs<ConfirmModalProps> => {
|
|
139
|
+
if (typeof messageOrOptions !== 'string') {
|
|
140
|
+
return {
|
|
141
|
+
...(messageOrOptions ?? {}),
|
|
142
|
+
message: messageOrTitle,
|
|
143
|
+
required: !!messageOrOptions?.required,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
...(options ?? {}),
|
|
149
|
+
title: messageOrTitle,
|
|
150
|
+
message: messageOrOptions,
|
|
151
|
+
required: !!options?.required,
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const properties = getProperties();
|
|
156
|
+
const result = await this.modalForm(this.requireComponent('confirm-modal'), properties);
|
|
157
|
+
const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
|
|
158
|
+
const checkboxes =
|
|
159
|
+
typeof result === 'object'
|
|
160
|
+
? result[1]
|
|
161
|
+
: Object.entries(properties.checkboxes ?? {}).reduce(
|
|
162
|
+
(values, [checkbox, { default: defaultValue }]) => ({
|
|
163
|
+
[checkbox]: defaultValue ?? false,
|
|
164
|
+
...values,
|
|
165
|
+
}),
|
|
166
|
+
{} as Record<string, boolean>,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
|
|
170
|
+
if (!checkbox.required || checkboxes[name]) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (confirmed && isDevelopment()) {
|
|
175
|
+
// eslint-disable-next-line no-console
|
|
176
|
+
console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return [false, checkboxes];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
186
|
+
public async prompt(title: string, message: string, options?: PromptOptions): Promise<string | null>;
|
|
187
|
+
public async prompt(
|
|
188
|
+
messageOrTitle: string,
|
|
189
|
+
messageOrOptions?: string | PromptOptions,
|
|
190
|
+
options?: PromptOptions,
|
|
191
|
+
): Promise<string | null> {
|
|
192
|
+
const trim = options?.trim ?? true;
|
|
193
|
+
const getProperties = (): PromptModalProps => {
|
|
194
|
+
if (typeof messageOrOptions !== 'string') {
|
|
195
|
+
return {
|
|
196
|
+
message: messageOrTitle,
|
|
197
|
+
...(messageOrOptions ?? {}),
|
|
198
|
+
} as PromptModalProps;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
title: messageOrTitle,
|
|
203
|
+
message: messageOrOptions,
|
|
204
|
+
...(options ?? {}),
|
|
205
|
+
} as PromptModalProps;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const rawResult = await this.modalForm(this.requireComponent('prompt-modal'), getProperties());
|
|
209
|
+
const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
|
|
210
|
+
|
|
211
|
+
return result ?? null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
|
|
215
|
+
public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
|
|
216
|
+
public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
|
|
217
|
+
public async loading<T>(
|
|
218
|
+
operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
|
|
219
|
+
operation?: Promise<T> | (() => T),
|
|
220
|
+
): Promise<T> {
|
|
221
|
+
const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
|
|
222
|
+
const processArgs = (): {
|
|
223
|
+
operationPromise: Promise<T>;
|
|
224
|
+
props?: AcceptRefs<LoadingModalProps>;
|
|
225
|
+
delay?: number;
|
|
226
|
+
} => {
|
|
227
|
+
if (typeof operationOrMessageOrOptions === 'string') {
|
|
228
|
+
return {
|
|
229
|
+
props: { message: operationOrMessageOrOptions },
|
|
230
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
|
|
235
|
+
return { operationPromise: processOperation(operationOrMessageOrOptions) };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const { delay, ...props } = operationOrMessageOrOptions;
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
props,
|
|
242
|
+
delay: unref(delay),
|
|
243
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
let delayed = false;
|
|
248
|
+
const { operationPromise, props, delay } = processArgs();
|
|
249
|
+
|
|
250
|
+
delay && (await Promise.race([after({ ms: delay }).then(() => (delayed = true)), operationPromise]));
|
|
251
|
+
|
|
252
|
+
if (delay && !delayed) {
|
|
253
|
+
return operationPromise;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const modal = await this.modal(this.requireComponent('loading-modal'), props);
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const result = await operationPromise;
|
|
260
|
+
|
|
261
|
+
await after({ ms: 500 });
|
|
262
|
+
|
|
263
|
+
return result;
|
|
264
|
+
} finally {
|
|
265
|
+
await this.closeModal(modal.id);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
public toast(message: string, options: ToastOptions = {}): void {
|
|
270
|
+
const { component, ...otherOptions } = options;
|
|
271
|
+
const toast: UIToast = {
|
|
272
|
+
id: uuid(),
|
|
273
|
+
properties: { message, ...otherOptions },
|
|
274
|
+
component: markRaw(component ?? this.requireComponent('toast')),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
this.setState('toasts', this.toasts.concat(toast));
|
|
38
278
|
}
|
|
39
279
|
|
|
40
|
-
public
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
280
|
+
public modal<T extends Component>(
|
|
281
|
+
...args: {} extends ComponentProps<T>
|
|
282
|
+
? [component: T, props?: AcceptRefs<ComponentProps<T>>]
|
|
283
|
+
: [component: T, props: AcceptRefs<ComponentProps<T>>]
|
|
284
|
+
): Promise<UIModal<ModalResult<T>>>;
|
|
285
|
+
|
|
286
|
+
public async modal<T extends Component>(component: T, props?: ComponentProps<T>): Promise<UIModal<ModalResult<T>>> {
|
|
44
287
|
const id = uuid();
|
|
45
|
-
const callbacks: Partial<ModalCallbacks<ModalResult<
|
|
46
|
-
const modal:
|
|
288
|
+
const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
|
|
289
|
+
const modal: UIModal<ModalResult<T>> = {
|
|
47
290
|
id,
|
|
48
|
-
|
|
291
|
+
closing: false,
|
|
292
|
+
properties: props ?? {},
|
|
49
293
|
component: markRaw(component),
|
|
50
294
|
beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
|
|
51
|
-
afterClose: new Promise((resolve) => (callbacks.
|
|
295
|
+
afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
|
|
52
296
|
};
|
|
53
|
-
const activeModal = this.modals.at(-1);
|
|
54
297
|
const modals = this.modals.concat(modal);
|
|
55
298
|
|
|
56
299
|
this.modalCallbacks[modal.id] = callbacks;
|
|
@@ -58,61 +301,115 @@ export class UIService extends Service {
|
|
|
58
301
|
this.setState({ modals });
|
|
59
302
|
|
|
60
303
|
await nextTick();
|
|
61
|
-
await (activeModal && Events.emit('hide-modal', { id: activeModal.id }));
|
|
62
|
-
await Promise.all([
|
|
63
|
-
activeModal || Events.emit('show-overlays-backdrop'),
|
|
64
|
-
Events.emit('show-modal', { id: modal.id }),
|
|
65
|
-
]);
|
|
66
304
|
|
|
67
305
|
return modal;
|
|
68
306
|
}
|
|
69
307
|
|
|
308
|
+
public modalForm<T extends Component>(
|
|
309
|
+
...args: {} extends ComponentProps<T>
|
|
310
|
+
? [component: T, props?: AcceptRefs<ComponentProps<T>>]
|
|
311
|
+
: [component: T, props: AcceptRefs<ComponentProps<T>>]
|
|
312
|
+
): Promise<ModalResult<T> | undefined>;
|
|
313
|
+
|
|
314
|
+
public async modalForm<T extends Component>(
|
|
315
|
+
component: T,
|
|
316
|
+
props?: ComponentProps<T>,
|
|
317
|
+
): Promise<ModalResult<T> | undefined> {
|
|
318
|
+
const modal = await this.modal<T>(component, props as ComponentProps<T>);
|
|
319
|
+
const result = await modal.beforeClose;
|
|
320
|
+
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
|
|
70
324
|
public async closeModal(id: string, result?: unknown): Promise<void> {
|
|
325
|
+
if (!App.isMounted()) {
|
|
326
|
+
await this.removeModal(id, result);
|
|
327
|
+
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
71
331
|
await Events.emit('close-modal', { id, result });
|
|
72
332
|
}
|
|
73
333
|
|
|
74
|
-
|
|
75
|
-
|
|
334
|
+
public async closeAllModals(): Promise<void> {
|
|
335
|
+
while (this.modals.length > 0) {
|
|
336
|
+
await this.closeModal(required(this.modals[this.modals.length - 1]).id);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
76
339
|
|
|
340
|
+
protected override async boot(): Promise<void> {
|
|
77
341
|
this.watchModalEvents();
|
|
342
|
+
this.watchMountedEvent();
|
|
343
|
+
this.watchViewportBreakpoints();
|
|
78
344
|
}
|
|
79
345
|
|
|
80
|
-
private
|
|
81
|
-
|
|
346
|
+
private async removeModal(id: string, result?: unknown): Promise<void> {
|
|
347
|
+
this.setState(
|
|
348
|
+
'modals',
|
|
349
|
+
this.modals.filter((m) => m.id !== id),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
this.modalCallbacks[id]?.hasClosed?.(result);
|
|
353
|
+
|
|
354
|
+
delete this.modalCallbacks[id];
|
|
82
355
|
}
|
|
83
356
|
|
|
84
357
|
private watchModalEvents(): void {
|
|
85
|
-
Events.on('modal-will-close', ({ modal, result }) => {
|
|
86
|
-
this.
|
|
358
|
+
Events.on('modal-will-close', ({ modal: { id }, result }) => {
|
|
359
|
+
const modal = this.modals.find((_modal) => id === _modal.id);
|
|
87
360
|
|
|
88
|
-
if (
|
|
89
|
-
|
|
361
|
+
if (modal) {
|
|
362
|
+
modal.closing = true;
|
|
90
363
|
}
|
|
364
|
+
|
|
365
|
+
this.modalCallbacks[id]?.willClose?.(result);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
|
|
369
|
+
await this.removeModal(id, result);
|
|
91
370
|
});
|
|
371
|
+
}
|
|
92
372
|
|
|
93
|
-
|
|
94
|
-
|
|
373
|
+
private watchMountedEvent(): void {
|
|
374
|
+
Events.once('application-mounted', async () => {
|
|
375
|
+
if (!globalThis.document || !globalThis.getComputedStyle) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
95
378
|
|
|
96
|
-
|
|
379
|
+
const splash = globalThis.document.getElementById('splash');
|
|
380
|
+
|
|
381
|
+
if (!splash) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
97
384
|
|
|
98
|
-
|
|
385
|
+
if (globalThis.getComputedStyle(splash).opacity !== '0') {
|
|
386
|
+
splash.style.opacity = '0';
|
|
99
387
|
|
|
100
|
-
|
|
388
|
+
await after({ ms: 600 });
|
|
389
|
+
}
|
|
101
390
|
|
|
102
|
-
|
|
391
|
+
splash.remove();
|
|
103
392
|
});
|
|
104
393
|
}
|
|
105
394
|
|
|
395
|
+
private watchViewportBreakpoints(): void {
|
|
396
|
+
if (!globalThis.matchMedia) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
|
|
401
|
+
|
|
402
|
+
media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
|
|
403
|
+
}
|
|
404
|
+
|
|
106
405
|
}
|
|
107
406
|
|
|
108
|
-
export default facade(
|
|
407
|
+
export default facade(UIService);
|
|
109
408
|
|
|
110
|
-
declare module '
|
|
409
|
+
declare module '@aerogel/core/services/Events' {
|
|
111
410
|
export interface EventsPayload {
|
|
112
|
-
'modal-will-close': { modal: Modal; result?: unknown };
|
|
113
|
-
'modal-closed': { modal: Modal; result?: unknown };
|
|
114
411
|
'close-modal': { id: string; result?: unknown };
|
|
115
|
-
'
|
|
116
|
-
'
|
|
412
|
+
'modal-will-close': { modal: UIModal; result?: unknown };
|
|
413
|
+
'modal-has-closed': { modal: UIModal; result?: unknown };
|
|
117
414
|
}
|
|
118
415
|
}
|