@aerogel/core 0.0.0-next.a56c0f4966eb71571173f8502f3f36d357ceebc7 → 0.0.0-next.aa6e27a9c197d1ee10c9fe018ee8c0fc6ff77767
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 +2004 -1034
- package/dist/aerogel-core.js +3223 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +27 -37
- package/src/bootstrap/bootstrap.test.ts +7 -11
- package/src/bootstrap/index.ts +36 -16
- package/src/bootstrap/options.ts +4 -1
- package/src/components/AppLayout.vue +16 -0
- package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
- package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +5 -6
- package/src/components/AppToasts.vue +16 -0
- package/src/components/composition.ts +23 -0
- package/src/components/contracts/AlertModal.ts +4 -0
- package/src/components/contracts/Button.ts +16 -0
- package/src/components/contracts/ConfirmModal.ts +41 -0
- package/src/components/contracts/DropdownMenu.ts +11 -0
- package/src/components/contracts/ErrorReportModal.ts +29 -0
- package/src/components/contracts/Input.ts +26 -0
- package/src/components/contracts/LoadingModal.ts +18 -0
- package/src/components/contracts/Modal.ts +13 -0
- package/src/components/contracts/PromptModal.ts +28 -0
- package/src/components/contracts/Select.ts +36 -0
- package/src/components/contracts/Toast.ts +13 -0
- package/src/components/contracts/index.ts +9 -0
- package/src/components/contracts/shared.ts +9 -0
- package/src/components/headless/HeadlessButton.vue +50 -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/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +17 -19
- package/src/components/headless/HeadlessModalContent.vue +24 -0
- package/src/components/headless/HeadlessModalOverlay.vue +12 -0
- package/src/components/headless/HeadlessModalTitle.vue +12 -0
- package/src/components/headless/HeadlessSelect.vue +105 -0
- package/src/components/headless/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +5 -6
- package/src/components/headless/HeadlessSelectLabel.vue +25 -0
- package/src/components/headless/HeadlessSelectOption.vue +34 -0
- package/src/components/headless/HeadlessSelectOptions.vue +30 -0
- package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
- package/src/components/headless/HeadlessSelectValue.vue +15 -0
- package/src/components/headless/HeadlessToast.vue +18 -0
- package/src/components/headless/HeadlessToastAction.vue +13 -0
- package/src/components/headless/index.ts +18 -3
- package/src/components/index.ts +5 -9
- package/src/components/ui/AdvancedOptions.vue +18 -0
- package/src/components/ui/AlertModal.vue +13 -0
- package/src/components/ui/Button.vue +98 -0
- package/src/components/ui/Checkbox.vue +56 -0
- package/src/components/ui/ConfirmModal.vue +42 -0
- package/src/components/ui/DropdownMenu.vue +33 -0
- package/src/components/ui/EditableContent.vue +82 -0
- package/src/components/ui/ErrorMessage.vue +15 -0
- package/src/components/ui/ErrorReportModal.vue +62 -0
- package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +38 -29
- 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 +32 -0
- package/src/components/ui/Markdown.vue +69 -0
- package/src/components/ui/Modal.vue +70 -0
- package/src/components/ui/ModalContext.vue +30 -0
- package/src/components/ui/ProgressBar.vue +50 -0
- package/src/components/ui/PromptModal.vue +35 -0
- package/src/components/ui/Select.vue +21 -0
- package/src/components/ui/SelectLabel.vue +10 -0
- package/src/components/ui/SelectOptions.vue +31 -0
- package/src/components/ui/SelectTrigger.vue +29 -0
- package/src/components/ui/SettingsModal.vue +51 -0
- package/src/components/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +8 -8
- package/src/components/ui/Toast.vue +42 -0
- package/src/components/ui/index.ts +27 -0
- package/src/components/utils.ts +107 -0
- package/src/directives/index.ts +13 -5
- package/src/directives/measure.ts +40 -0
- package/src/errors/Errors.state.ts +1 -1
- package/src/errors/Errors.ts +35 -34
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +11 -17
- package/src/errors/utils.ts +35 -0
- package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
- package/src/forms/{Form.ts → FormController.ts} +86 -25
- package/src/forms/composition.ts +4 -4
- package/src/forms/index.ts +3 -1
- package/src/forms/utils.ts +36 -5
- package/src/forms/validation.ts +19 -0
- package/src/index.css +41 -0
- package/src/{main.ts → index.ts} +3 -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 +44 -29
- package/src/lang/index.ts +8 -6
- package/src/plugins/Plugin.ts +1 -1
- package/src/plugins/index.ts +10 -7
- package/src/services/App.state.ts +27 -5
- package/src/services/App.ts +32 -6
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.test.ts +39 -0
- package/src/services/Events.ts +112 -32
- package/src/services/Service.ts +154 -49
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +15 -6
- package/src/services/store.ts +8 -5
- 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 +19 -7
- package/src/ui/UI.ts +252 -63
- package/src/ui/index.ts +23 -17
- package/src/ui/utils.ts +16 -0
- package/src/utils/composition/events.ts +3 -2
- package/src/utils/composition/forms.ts +4 -3
- package/src/utils/composition/persistent.test.ts +33 -0
- package/src/utils/composition/persistent.ts +11 -0
- package/src/utils/composition/state.test.ts +47 -0
- package/src/utils/composition/state.ts +24 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/markdown.test.ts +50 -0
- package/src/utils/markdown.ts +19 -6
- package/src/utils/tailwindcss.test.ts +26 -0
- package/src/utils/tailwindcss.ts +7 -0
- package/src/utils/vdom.ts +31 -0
- package/src/utils/vue.ts +31 -19
- package/dist/aerogel-core.cjs.js +0 -2
- package/dist/aerogel-core.cjs.js.map +0 -1
- package/dist/aerogel-core.esm.js +0 -2
- package/dist/aerogel-core.esm.js.map +0 -1
- package/histoire.config.ts +0 -7
- package/noeldemartin.config.js +0 -5
- package/postcss.config.js +0 -6
- package/src/assets/histoire.css +0 -3
- package/src/components/AGAppLayout.vue +0 -15
- package/src/components/AGAppSnackbars.vue +0 -13
- package/src/components/constants.ts +0 -8
- package/src/components/forms/AGButton.vue +0 -44
- package/src/components/forms/AGCheckbox.vue +0 -35
- package/src/components/forms/AGForm.vue +0 -26
- package/src/components/forms/AGInput.vue +0 -36
- package/src/components/forms/AGSelect.story.vue +0 -28
- package/src/components/forms/AGSelect.vue +0 -53
- 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 -45
- package/src/components/headless/forms/AGHeadlessInputLabel.vue +0 -16
- package/src/components/headless/forms/AGHeadlessSelect.ts +0 -43
- package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
- package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -21
- package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
- package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
- package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
- package/src/components/headless/forms/index.ts +0 -14
- package/src/components/headless/modals/AGHeadlessModal.ts +0 -7
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
- package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
- package/src/components/headless/modals/index.ts +0 -6
- package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
- package/src/components/headless/snackbars/index.ts +0 -25
- package/src/components/lib/AGErrorMessage.vue +0 -16
- package/src/components/lib/AGLink.vue +0 -9
- package/src/components/lib/AGMarkdown.vue +0 -36
- package/src/components/lib/index.ts +0 -4
- package/src/components/modals/AGAlertModal.vue +0 -25
- package/src/components/modals/AGConfirmModal.vue +0 -30
- package/src/components/modals/AGErrorReportModal.ts +0 -20
- package/src/components/modals/AGErrorReportModal.vue +0 -62
- package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
- package/src/components/modals/AGLoadingModal.vue +0 -19
- package/src/components/modals/AGModal.ts +0 -10
- package/src/components/modals/AGModal.vue +0 -37
- package/src/components/modals/AGModalContext.ts +0 -8
- package/src/components/modals/AGModalContext.vue +0 -22
- package/src/components/modals/AGModalTitle.vue +0 -9
- package/src/components/modals/index.ts +0 -23
- package/src/components/snackbars/AGSnackbar.vue +0 -42
- package/src/components/snackbars/index.ts +0 -3
- package/src/directives/initial-focus.ts +0 -11
- package/src/main.histoire.ts +0 -1
- package/tailwind.config.js +0 -4
- package/tsconfig.json +0 -11
- package/vite.config.ts +0 -14
|
@@ -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/ui/UI.state.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Component } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { defineServiceState } from '
|
|
3
|
+
import { defineServiceState } from '@aerogel/core/services/Service';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import { Layouts, getCurrentLayout } from './utils';
|
|
6
|
+
|
|
7
|
+
export interface UIModal<T = unknown> {
|
|
6
8
|
id: string;
|
|
7
9
|
properties: Record<string, unknown>;
|
|
8
10
|
component: Component;
|
|
@@ -10,14 +12,19 @@ export interface Modal<T = unknown> {
|
|
|
10
12
|
afterClose: Promise<T | undefined>;
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
export interface UIModalContext {
|
|
16
|
+
modal: UIModal;
|
|
17
|
+
childIndex?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
export interface ModalComponent<
|
|
14
21
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
15
|
-
Properties extends
|
|
22
|
+
Properties extends object = object,
|
|
16
23
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
17
|
-
Result = unknown
|
|
24
|
+
Result = unknown,
|
|
18
25
|
> {}
|
|
19
26
|
|
|
20
|
-
export interface
|
|
27
|
+
export interface UIToast {
|
|
21
28
|
id: string;
|
|
22
29
|
component: Component;
|
|
23
30
|
properties: Record<string, unknown>;
|
|
@@ -26,7 +33,12 @@ export interface Snackbar {
|
|
|
26
33
|
export default defineServiceState({
|
|
27
34
|
name: 'ui',
|
|
28
35
|
initialState: {
|
|
29
|
-
modals: [] as
|
|
30
|
-
|
|
36
|
+
modals: [] as UIModal[],
|
|
37
|
+
toasts: [] as UIToast[],
|
|
38
|
+
layout: getCurrentLayout(),
|
|
39
|
+
},
|
|
40
|
+
computed: {
|
|
41
|
+
mobile: ({ layout }) => layout === Layouts.Mobile,
|
|
42
|
+
desktop: ({ layout }) => layout === Layouts.Desktop,
|
|
31
43
|
},
|
|
32
44
|
});
|
package/src/ui/UI.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import { after, facade, fail, uuid } from '@noeldemartin/utils';
|
|
1
|
+
import { after, facade, fail, isDevelopment, required, uuid } from '@noeldemartin/utils';
|
|
2
2
|
import { markRaw, nextTick } from 'vue';
|
|
3
3
|
import type { Component } from 'vue';
|
|
4
4
|
import type { ObjectValues } from '@noeldemartin/utils';
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import
|
|
6
|
+
import App from '@aerogel/core/services/App';
|
|
7
|
+
import Events from '@aerogel/core/services/Events';
|
|
8
|
+
import type { AcceptRefs } from '@aerogel/core/utils';
|
|
9
|
+
import type { AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
|
|
10
|
+
import type { ButtonVariant } from '@aerogel/core/components/contracts/Button';
|
|
11
|
+
import type { ConfirmModalCheckboxes, ConfirmModalProps } from '@aerogel/core/components/contracts/ConfirmModal';
|
|
12
|
+
import type { LoadingModalProps } from '@aerogel/core/components/contracts/LoadingModal';
|
|
13
|
+
import type { PromptModalProps } from '@aerogel/core/components/contracts/PromptModal';
|
|
14
|
+
import type { ToastAction, ToastVariant } from '@aerogel/core/components/contracts/Toast';
|
|
8
15
|
|
|
9
16
|
import Service from './UI.state';
|
|
10
|
-
import
|
|
17
|
+
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
18
|
+
import type { ModalComponent, UIModal, UIToast } from './UI.state';
|
|
11
19
|
|
|
12
20
|
interface ModalCallbacks<T = unknown> {
|
|
13
21
|
willClose(result: T | undefined): void;
|
|
@@ -15,24 +23,56 @@ interface ModalCallbacks<T = unknown> {
|
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
|
|
18
|
-
type ModalResult<TComponent> =
|
|
19
|
-
? TResult
|
|
20
|
-
: never;
|
|
26
|
+
type ModalResult<TComponent> =
|
|
27
|
+
TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
|
|
21
28
|
|
|
22
29
|
export const UIComponents = {
|
|
23
30
|
AlertModal: 'alert-modal',
|
|
24
31
|
ConfirmModal: 'confirm-modal',
|
|
25
32
|
ErrorReportModal: 'error-report-modal',
|
|
26
33
|
LoadingModal: 'loading-modal',
|
|
27
|
-
|
|
34
|
+
PromptModal: 'prompt-modal',
|
|
35
|
+
Toast: 'toast',
|
|
36
|
+
StartupCrash: 'startup-crash',
|
|
28
37
|
} as const;
|
|
29
38
|
|
|
30
39
|
export type UIComponent = ObjectValues<typeof UIComponents>;
|
|
31
40
|
|
|
32
|
-
export
|
|
41
|
+
export type ConfirmOptions = AcceptRefs<{
|
|
42
|
+
acceptText?: string;
|
|
43
|
+
acceptVariant?: ButtonVariant;
|
|
44
|
+
cancelText?: string;
|
|
45
|
+
cancelVariant?: ButtonVariant;
|
|
46
|
+
actions?: Record<string, () => unknown>;
|
|
47
|
+
required?: boolean;
|
|
48
|
+
}>;
|
|
49
|
+
|
|
50
|
+
export type LoadingOptions = AcceptRefs<{
|
|
51
|
+
title?: string;
|
|
52
|
+
message?: string;
|
|
53
|
+
progress?: number;
|
|
54
|
+
}>;
|
|
55
|
+
|
|
56
|
+
export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
|
|
57
|
+
extends ConfirmOptions {
|
|
58
|
+
checkboxes?: T;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type PromptOptions = AcceptRefs<{
|
|
62
|
+
label?: string;
|
|
63
|
+
defaultValue?: string;
|
|
64
|
+
placeholder?: string;
|
|
65
|
+
acceptText?: string;
|
|
66
|
+
acceptVariant?: ButtonVariant;
|
|
67
|
+
cancelText?: string;
|
|
68
|
+
cancelVariant?: ButtonVariant;
|
|
69
|
+
trim?: boolean;
|
|
70
|
+
}>;
|
|
71
|
+
|
|
72
|
+
export interface ToastOptions {
|
|
33
73
|
component?: Component;
|
|
34
|
-
|
|
35
|
-
actions?:
|
|
74
|
+
variant?: ToastVariant;
|
|
75
|
+
actions?: ToastAction[];
|
|
36
76
|
}
|
|
37
77
|
|
|
38
78
|
export class UIService extends Service {
|
|
@@ -47,34 +87,158 @@ export class UIService extends Service {
|
|
|
47
87
|
public alert(message: string): void;
|
|
48
88
|
public alert(title: string, message: string): void;
|
|
49
89
|
public alert(messageOrTitle: string, message?: string): void {
|
|
50
|
-
const
|
|
90
|
+
const getProperties = (): AlertModalProps => {
|
|
91
|
+
if (typeof message !== 'string') {
|
|
92
|
+
return { message: messageOrTitle };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
title: messageOrTitle,
|
|
97
|
+
message,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
51
100
|
|
|
52
|
-
this.openModal(
|
|
101
|
+
this.openModal<ModalComponent<AlertModalProps>>(
|
|
102
|
+
this.requireComponent(UIComponents.AlertModal),
|
|
103
|
+
getProperties(),
|
|
104
|
+
);
|
|
53
105
|
}
|
|
54
106
|
|
|
55
|
-
|
|
56
|
-
public async confirm(
|
|
57
|
-
public async confirm(
|
|
58
|
-
|
|
59
|
-
|
|
107
|
+
/* eslint-disable max-len */
|
|
108
|
+
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
109
|
+
public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
110
|
+
public async confirm<T extends ConfirmModalCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
111
|
+
public async confirm<T extends ConfirmModalCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
112
|
+
/* eslint-enable max-len */
|
|
113
|
+
|
|
114
|
+
public async confirm(
|
|
115
|
+
messageOrTitle: string,
|
|
116
|
+
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
117
|
+
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
118
|
+
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
119
|
+
const getProperties = (): AcceptRefs<ConfirmModalProps> => {
|
|
120
|
+
if (typeof messageOrOptions !== 'string') {
|
|
121
|
+
return {
|
|
122
|
+
...(messageOrOptions ?? {}),
|
|
123
|
+
message: messageOrTitle,
|
|
124
|
+
required: !!messageOrOptions?.required,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
...(options ?? {}),
|
|
130
|
+
title: messageOrTitle,
|
|
131
|
+
message: messageOrOptions,
|
|
132
|
+
required: !!options?.required,
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
type ConfirmModalComponent = ModalComponent<
|
|
137
|
+
AcceptRefs<ConfirmModalProps>,
|
|
138
|
+
boolean | [boolean, Record<string, boolean>]
|
|
139
|
+
>;
|
|
140
|
+
|
|
141
|
+
const properties = getProperties();
|
|
142
|
+
const modal = await this.openModal<ConfirmModalComponent>(
|
|
60
143
|
this.requireComponent(UIComponents.ConfirmModal),
|
|
61
|
-
|
|
144
|
+
properties,
|
|
62
145
|
);
|
|
63
146
|
const result = await modal.beforeClose;
|
|
64
147
|
|
|
65
|
-
|
|
148
|
+
const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
|
|
149
|
+
const checkboxes =
|
|
150
|
+
typeof result === 'object'
|
|
151
|
+
? result[1]
|
|
152
|
+
: Object.entries(properties.checkboxes ?? {}).reduce(
|
|
153
|
+
(values, [checkbox, { default: defaultValue }]) => ({
|
|
154
|
+
[checkbox]: defaultValue ?? false,
|
|
155
|
+
...values,
|
|
156
|
+
}),
|
|
157
|
+
{} as Record<string, boolean>,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
|
|
161
|
+
if (!checkbox.required || checkboxes[name]) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (confirmed && isDevelopment()) {
|
|
166
|
+
// eslint-disable-next-line no-console
|
|
167
|
+
console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return [false, checkboxes];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
177
|
+
public async prompt(title: string, message: string, options?: PromptOptions): Promise<string | null>;
|
|
178
|
+
public async prompt(
|
|
179
|
+
messageOrTitle: string,
|
|
180
|
+
messageOrOptions?: string | PromptOptions,
|
|
181
|
+
options?: PromptOptions,
|
|
182
|
+
): Promise<string | null> {
|
|
183
|
+
const trim = options?.trim ?? true;
|
|
184
|
+
const getProperties = (): PromptModalProps => {
|
|
185
|
+
if (typeof messageOrOptions !== 'string') {
|
|
186
|
+
return {
|
|
187
|
+
message: messageOrTitle,
|
|
188
|
+
...(messageOrOptions ?? {}),
|
|
189
|
+
} as PromptModalProps;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
title: messageOrTitle,
|
|
194
|
+
message: messageOrOptions,
|
|
195
|
+
...(options ?? {}),
|
|
196
|
+
} as PromptModalProps;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
|
|
200
|
+
this.requireComponent(UIComponents.PromptModal),
|
|
201
|
+
getProperties(),
|
|
202
|
+
);
|
|
203
|
+
const rawResult = await modal.beforeClose;
|
|
204
|
+
const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
|
|
205
|
+
|
|
206
|
+
return result ?? null;
|
|
66
207
|
}
|
|
67
208
|
|
|
68
|
-
public async loading<T>(operation: Promise<T>): Promise<T>;
|
|
69
|
-
public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
|
|
70
|
-
public async loading<T>(
|
|
71
|
-
|
|
209
|
+
public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
|
|
210
|
+
public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
|
|
211
|
+
public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
|
|
212
|
+
public async loading<T>(
|
|
213
|
+
operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
|
|
214
|
+
operation?: Promise<T> | (() => T),
|
|
215
|
+
): Promise<T> {
|
|
216
|
+
const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
|
|
217
|
+
const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
|
|
218
|
+
if (typeof operationOrMessageOrOptions === 'string') {
|
|
219
|
+
return {
|
|
220
|
+
props: { message: operationOrMessageOrOptions },
|
|
221
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
|
|
226
|
+
return { operationPromise: processOperation(operationOrMessageOrOptions) };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
props: operationOrMessageOrOptions,
|
|
231
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
232
|
+
};
|
|
233
|
+
};
|
|
72
234
|
|
|
73
|
-
const
|
|
74
|
-
const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal),
|
|
235
|
+
const { operationPromise, props } = processArgs();
|
|
236
|
+
const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
|
|
75
237
|
|
|
76
238
|
try {
|
|
77
|
-
const
|
|
239
|
+
const result = await operationPromise;
|
|
240
|
+
|
|
241
|
+
await after({ ms: 500 });
|
|
78
242
|
|
|
79
243
|
return result;
|
|
80
244
|
} finally {
|
|
@@ -82,23 +246,15 @@ export class UIService extends Service {
|
|
|
82
246
|
}
|
|
83
247
|
}
|
|
84
248
|
|
|
85
|
-
public
|
|
86
|
-
const
|
|
249
|
+
public toast(message: string, options: ToastOptions = {}): void {
|
|
250
|
+
const { component, ...otherOptions } = options;
|
|
251
|
+
const toast: UIToast = {
|
|
87
252
|
id: uuid(),
|
|
88
|
-
properties: { message, ...
|
|
89
|
-
component:
|
|
253
|
+
properties: { message, ...otherOptions },
|
|
254
|
+
component: markRaw(component ?? this.requireComponent(UIComponents.Toast)),
|
|
90
255
|
};
|
|
91
256
|
|
|
92
|
-
this.setState('
|
|
93
|
-
|
|
94
|
-
setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
public hideSnackbar(id: string): void {
|
|
98
|
-
this.setState(
|
|
99
|
-
'snackbars',
|
|
100
|
-
this.snackbars.filter((snackbar) => snackbar.id !== id),
|
|
101
|
-
);
|
|
257
|
+
this.setState('toasts', this.toasts.concat(toast));
|
|
102
258
|
}
|
|
103
259
|
|
|
104
260
|
public registerComponent(name: UIComponent, component: Component): void {
|
|
@@ -108,10 +264,10 @@ export class UIService extends Service {
|
|
|
108
264
|
public async openModal<TModalComponent extends ModalComponent>(
|
|
109
265
|
component: TModalComponent,
|
|
110
266
|
properties?: ModalProperties<TModalComponent>,
|
|
111
|
-
): Promise<
|
|
267
|
+
): Promise<UIModal<ModalResult<TModalComponent>>> {
|
|
112
268
|
const id = uuid();
|
|
113
269
|
const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
|
|
114
|
-
const modal:
|
|
270
|
+
const modal: UIModal<ModalResult<TModalComponent>> = {
|
|
115
271
|
id,
|
|
116
272
|
properties: properties ?? {},
|
|
117
273
|
component: markRaw(component),
|
|
@@ -136,12 +292,40 @@ export class UIService extends Service {
|
|
|
136
292
|
}
|
|
137
293
|
|
|
138
294
|
public async closeModal(id: string, result?: unknown): Promise<void> {
|
|
295
|
+
if (!App.isMounted()) {
|
|
296
|
+
await this.removeModal(id, result);
|
|
297
|
+
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
139
301
|
await Events.emit('close-modal', { id, result });
|
|
140
302
|
}
|
|
141
303
|
|
|
142
|
-
|
|
304
|
+
public async closeAllModals(): Promise<void> {
|
|
305
|
+
while (this.modals.length > 0) {
|
|
306
|
+
await this.closeModal(required(this.modals[this.modals.length - 1]).id);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
protected override async boot(): Promise<void> {
|
|
143
311
|
this.watchModalEvents();
|
|
144
312
|
this.watchMountedEvent();
|
|
313
|
+
this.watchViewportBreakpoints();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private async removeModal(id: string, result?: unknown): Promise<void> {
|
|
317
|
+
this.setState(
|
|
318
|
+
'modals',
|
|
319
|
+
this.modals.filter((m) => m.id !== id),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
this.modalCallbacks[id]?.closed?.(result);
|
|
323
|
+
|
|
324
|
+
delete this.modalCallbacks[id];
|
|
325
|
+
|
|
326
|
+
const activeModal = this.modals.at(-1);
|
|
327
|
+
|
|
328
|
+
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
145
329
|
}
|
|
146
330
|
|
|
147
331
|
private watchModalEvents(): void {
|
|
@@ -153,31 +337,24 @@ export class UIService extends Service {
|
|
|
153
337
|
}
|
|
154
338
|
});
|
|
155
339
|
|
|
156
|
-
Events.on('modal-closed', async ({ modal, result }) => {
|
|
157
|
-
this.
|
|
158
|
-
'modals',
|
|
159
|
-
this.modals.filter((m) => m.id !== modal.id),
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
this.modalCallbacks[modal.id]?.closed?.(result);
|
|
163
|
-
|
|
164
|
-
delete this.modalCallbacks[modal.id];
|
|
165
|
-
|
|
166
|
-
const activeModal = this.modals.at(-1);
|
|
167
|
-
|
|
168
|
-
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
340
|
+
Events.on('modal-closed', async ({ modal: { id }, result }) => {
|
|
341
|
+
await this.removeModal(id, result);
|
|
169
342
|
});
|
|
170
343
|
}
|
|
171
344
|
|
|
172
345
|
private watchMountedEvent(): void {
|
|
173
346
|
Events.once('application-mounted', async () => {
|
|
174
|
-
|
|
347
|
+
if (!globalThis.document || !globalThis.getComputedStyle) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const splash = globalThis.document.getElementById('splash');
|
|
175
352
|
|
|
176
353
|
if (!splash) {
|
|
177
354
|
return;
|
|
178
355
|
}
|
|
179
356
|
|
|
180
|
-
if (
|
|
357
|
+
if (globalThis.getComputedStyle(splash).opacity !== '0') {
|
|
181
358
|
splash.style.opacity = '0';
|
|
182
359
|
|
|
183
360
|
await after({ ms: 600 });
|
|
@@ -187,16 +364,28 @@ export class UIService extends Service {
|
|
|
187
364
|
});
|
|
188
365
|
}
|
|
189
366
|
|
|
367
|
+
private watchViewportBreakpoints(): void {
|
|
368
|
+
if (!globalThis.matchMedia) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
|
|
373
|
+
|
|
374
|
+
media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
|
|
375
|
+
}
|
|
376
|
+
|
|
190
377
|
}
|
|
191
378
|
|
|
192
|
-
export default facade(
|
|
379
|
+
export default facade(UIService);
|
|
193
380
|
|
|
194
|
-
declare module '
|
|
381
|
+
declare module '@aerogel/core/services/Events' {
|
|
195
382
|
export interface EventsPayload {
|
|
196
|
-
'modal-will-close': { modal: Modal; result?: unknown };
|
|
197
|
-
'modal-closed': { modal: Modal; result?: unknown };
|
|
198
383
|
'close-modal': { id: string; result?: unknown };
|
|
199
384
|
'hide-modal': { id: string };
|
|
385
|
+
'hide-overlays-backdrop': void;
|
|
386
|
+
'modal-closed': { modal: UIModal; result?: unknown };
|
|
387
|
+
'modal-will-close': { modal: UIModal; result?: unknown };
|
|
200
388
|
'show-modal': { id: string };
|
|
389
|
+
'show-overlays-backdrop': void;
|
|
201
390
|
}
|
|
202
391
|
}
|
package/src/ui/index.ts
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
import type { Component } from 'vue';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import AlertModal from '@aerogel/core/components/ui/AlertModal.vue';
|
|
4
|
+
import ConfirmModal from '@aerogel/core/components/ui/ConfirmModal.vue';
|
|
5
|
+
import ErrorReportModal from '@aerogel/core/components/ui/ErrorReportModal.vue';
|
|
6
|
+
import LoadingModal from '@aerogel/core/components/ui/LoadingModal.vue';
|
|
7
|
+
import PromptModal from '@aerogel/core/components/ui/PromptModal.vue';
|
|
8
|
+
import StartupCrash from '@aerogel/core/components/ui/StartupCrash.vue';
|
|
9
|
+
import Toast from '@aerogel/core/components/ui/Toast.vue';
|
|
10
|
+
import { bootServices } from '@aerogel/core/services';
|
|
11
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
5
12
|
|
|
6
13
|
import UI, { UIComponents } from './UI';
|
|
7
|
-
import AGAlertModal from '../components/modals/AGAlertModal.vue';
|
|
8
|
-
import AGConfirmModal from '../components/modals/AGConfirmModal.vue';
|
|
9
|
-
import AGErrorReportModal from '../components/modals/AGErrorReportModal.vue';
|
|
10
|
-
import AGLoadingModal from '../components/modals/AGLoadingModal.vue';
|
|
11
|
-
import AGSnackbar from '../components/snackbars/AGSnackbar.vue';
|
|
12
14
|
import type { UIComponent } from './UI';
|
|
13
15
|
|
|
14
|
-
export { UI, UIComponents, UIComponent };
|
|
15
|
-
|
|
16
16
|
const services = { $ui: UI };
|
|
17
17
|
|
|
18
|
+
export * from './UI';
|
|
19
|
+
export * from './utils';
|
|
20
|
+
export { default as UI } from './UI';
|
|
21
|
+
|
|
18
22
|
export type UIServices = typeof services;
|
|
19
23
|
|
|
20
24
|
export default definePlugin({
|
|
21
25
|
async install(app, options) {
|
|
22
26
|
const defaultComponents = {
|
|
23
|
-
[UIComponents.AlertModal]:
|
|
24
|
-
[UIComponents.ConfirmModal]:
|
|
25
|
-
[UIComponents.ErrorReportModal]:
|
|
26
|
-
[UIComponents.LoadingModal]:
|
|
27
|
-
[UIComponents.
|
|
27
|
+
[UIComponents.AlertModal]: AlertModal,
|
|
28
|
+
[UIComponents.ConfirmModal]: ConfirmModal,
|
|
29
|
+
[UIComponents.ErrorReportModal]: ErrorReportModal,
|
|
30
|
+
[UIComponents.LoadingModal]: LoadingModal,
|
|
31
|
+
[UIComponents.PromptModal]: PromptModal,
|
|
32
|
+
[UIComponents.Toast]: Toast,
|
|
33
|
+
[UIComponents.StartupCrash]: StartupCrash,
|
|
28
34
|
};
|
|
29
35
|
|
|
30
36
|
Object.entries({
|
|
@@ -36,12 +42,12 @@ export default definePlugin({
|
|
|
36
42
|
},
|
|
37
43
|
});
|
|
38
44
|
|
|
39
|
-
declare module '
|
|
40
|
-
interface AerogelOptions {
|
|
45
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
46
|
+
export interface AerogelOptions {
|
|
41
47
|
components?: Partial<Record<UIComponent, Component>>;
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
declare module '
|
|
51
|
+
declare module '@aerogel/core/services' {
|
|
46
52
|
export interface Services extends UIServices {}
|
|
47
53
|
}
|
package/src/ui/utils.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const MOBILE_BREAKPOINT = 768;
|
|
2
|
+
|
|
3
|
+
export const Layouts = {
|
|
4
|
+
Mobile: 'mobile',
|
|
5
|
+
Desktop: 'desktop',
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
export type Layout = (typeof Layouts)[keyof typeof Layouts];
|
|
9
|
+
|
|
10
|
+
export function getCurrentLayout(): Layout {
|
|
11
|
+
if (globalThis.innerWidth > MOBILE_BREAKPOINT) {
|
|
12
|
+
return Layouts.Desktop;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return Layouts.Mobile;
|
|
16
|
+
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { onUnmounted } from 'vue';
|
|
2
2
|
|
|
3
|
-
import Events from '
|
|
3
|
+
import Events from '@aerogel/core/services/Events';
|
|
4
4
|
import type {
|
|
5
5
|
EventListener,
|
|
6
6
|
EventWithPayload,
|
|
7
7
|
EventWithoutPayload,
|
|
8
8
|
EventsPayload,
|
|
9
9
|
UnknownEvent,
|
|
10
|
-
} from '
|
|
10
|
+
} from '@aerogel/core/services/Events';
|
|
11
11
|
|
|
12
12
|
export function useEvent<Event extends EventWithoutPayload>(event: Event, listener: () => unknown): void;
|
|
13
13
|
export function useEvent<Event extends EventWithPayload>(
|
|
14
14
|
event: Event,
|
|
15
15
|
listener: EventListener<EventsPayload[Event]>
|
|
16
16
|
): void;
|
|
17
|
+
export function useEvent<Payload>(event: string, listener: (payload: Payload) => unknown): void;
|
|
17
18
|
export function useEvent<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): void;
|
|
18
19
|
|
|
19
20
|
export function useEvent(event: string, listener: EventListener): void {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { objectWithout } from '@noeldemartin/utils';
|
|
2
2
|
import { computed, useAttrs } from 'vue';
|
|
3
|
+
import type { ClassValue } from 'clsx';
|
|
3
4
|
import type { ComputedRef } from 'vue';
|
|
4
5
|
|
|
5
|
-
export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<
|
|
6
|
+
export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<ClassValue>] {
|
|
6
7
|
const attrs = useAttrs();
|
|
7
|
-
const
|
|
8
|
+
const classes = computed(() => attrs.class);
|
|
8
9
|
const inputAttrs = computed(() => objectWithout(attrs, 'class'));
|
|
9
10
|
|
|
10
|
-
return [inputAttrs,
|
|
11
|
+
return [inputAttrs, classes as ComputedRef<ClassValue>];
|
|
11
12
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { nextTick } from 'vue';
|
|
3
|
+
import { Storage } from '@noeldemartin/utils';
|
|
4
|
+
|
|
5
|
+
import { persistent } from './persistent';
|
|
6
|
+
|
|
7
|
+
describe('Vue persistent helper', () => {
|
|
8
|
+
|
|
9
|
+
it('serializes to localStorage', async () => {
|
|
10
|
+
// Arrange
|
|
11
|
+
const store = persistent<{ foo?: string }>('foobar', {});
|
|
12
|
+
|
|
13
|
+
// Act
|
|
14
|
+
store.foo = 'bar';
|
|
15
|
+
|
|
16
|
+
await nextTick();
|
|
17
|
+
|
|
18
|
+
// Assert
|
|
19
|
+
expect(Storage.get('foobar')).toEqual({ foo: 'bar' });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('reads from localStorage', async () => {
|
|
23
|
+
// Arrange
|
|
24
|
+
Storage.set('foobar', { foo: 'bar' });
|
|
25
|
+
|
|
26
|
+
// Act
|
|
27
|
+
const store = persistent<{ foo?: string }>('foobar', {});
|
|
28
|
+
|
|
29
|
+
// Assert
|
|
30
|
+
expect(store.foo).toEqual('bar');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { reactive, toRaw, watch } from 'vue';
|
|
2
|
+
import { Storage } from '@noeldemartin/utils';
|
|
3
|
+
import type { UnwrapNestedRefs } from 'vue';
|
|
4
|
+
|
|
5
|
+
export function persistent<T extends object>(name: string, defaults: T): UnwrapNestedRefs<T> {
|
|
6
|
+
const store = reactive<T>(Storage.get<T>(name) ?? defaults);
|
|
7
|
+
|
|
8
|
+
watch(store, () => Storage.set(name, toRaw(store)));
|
|
9
|
+
|
|
10
|
+
return store;
|
|
11
|
+
}
|