@aerogel/core 0.0.0-next.a68f133e2c9a1ae9ba84b4e2e42df909289e5fba → 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 +1948 -1460
- 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 +4 -8
- package/src/bootstrap/index.ts +26 -16
- package/src/bootstrap/options.ts +1 -1
- package/src/components/{AGAppLayout.vue → AppLayout.vue} +4 -4
- package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
- package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +5 -10
- 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/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
- package/src/components/headless/HeadlessInputTextArea.vue +40 -0
- package/src/components/headless/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +17 -17
- 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} +34 -27
- 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 +106 -9
- package/src/directives/index.ts +11 -5
- package/src/directives/measure.ts +34 -6
- package/src/errors/Errors.state.ts +1 -1
- package/src/errors/Errors.ts +24 -32
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +10 -6
- 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} +85 -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 +43 -28
- 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 +26 -3
- package/src/services/App.ts +11 -3
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.test.ts +39 -0
- package/src/services/Events.ts +111 -31
- package/src/services/Service.ts +129 -54
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +13 -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 +184 -76
- package/src/ui/index.ts +19 -18
- package/src/ui/utils.ts +16 -0
- package/src/utils/composition/events.ts +2 -2
- package/src/utils/composition/forms.ts +4 -3
- package/src/utils/composition/persistent.test.ts +33 -0
- package/src/utils/composition/persistent.ts +11 -0
- package/src/utils/composition/state.test.ts +47 -0
- package/src/utils/composition/state.ts +24 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/markdown.test.ts +50 -0
- package/src/utils/markdown.ts +19 -6
- package/src/utils/vdom.ts +31 -0
- package/src/utils/vue.ts +22 -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/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 -41
- package/src/components/forms/AGForm.vue +0 -26
- package/src/components/forms/AGInput.vue +0 -38
- package/src/components/forms/AGSelect.story.vue +0 -46
- package/src/components/forms/AGSelect.vue +0 -60
- 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 -28
- package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
- package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
- package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
- 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 -24
- 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 -34
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
- package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
- package/src/components/headless/modals/index.ts +0 -4
- package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
- package/src/components/headless/snackbars/index.ts +0 -40
- 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/AGMeasured.vue +0 -15
- package/src/components/lib/index.ts +0 -5
- package/src/components/modals/AGAlertModal.ts +0 -15
- package/src/components/modals/AGAlertModal.vue +0 -14
- package/src/components/modals/AGConfirmModal.ts +0 -27
- package/src/components/modals/AGConfirmModal.vue +0 -26
- package/src/components/modals/AGErrorReportModal.ts +0 -46
- package/src/components/modals/AGErrorReportModal.vue +0 -54
- package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
- package/src/components/modals/AGLoadingModal.ts +0 -23
- package/src/components/modals/AGLoadingModal.vue +0 -15
- package/src/components/modals/AGModal.ts +0 -10
- package/src/components/modals/AGModal.vue +0 -39
- package/src/components/modals/AGModalContext.ts +0 -8
- package/src/components/modals/AGModalContext.vue +0 -22
- package/src/components/modals/AGModalTitle.vue +0 -9
- package/src/components/modals/AGPromptModal.ts +0 -30
- package/src/components/modals/AGPromptModal.vue +0 -34
- package/src/components/modals/index.ts +0 -17
- package/src/components/snackbars/AGSnackbar.vue +0 -36
- 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,14 +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
|
|
8
|
-
import type {
|
|
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';
|
|
9
15
|
|
|
10
16
|
import Service from './UI.state';
|
|
11
|
-
import
|
|
17
|
+
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
18
|
+
import type { ModalComponent, UIModal, UIToast } from './UI.state';
|
|
12
19
|
|
|
13
20
|
interface ModalCallbacks<T = unknown> {
|
|
14
21
|
willClose(result: T | undefined): void;
|
|
@@ -16,9 +23,8 @@ interface ModalCallbacks<T = unknown> {
|
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
|
|
19
|
-
type ModalResult<TComponent> =
|
|
20
|
-
? TResult
|
|
21
|
-
: never;
|
|
26
|
+
type ModalResult<TComponent> =
|
|
27
|
+
TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
|
|
22
28
|
|
|
23
29
|
export const UIComponents = {
|
|
24
30
|
AlertModal: 'alert-modal',
|
|
@@ -26,29 +32,47 @@ export const UIComponents = {
|
|
|
26
32
|
ErrorReportModal: 'error-report-modal',
|
|
27
33
|
LoadingModal: 'loading-modal',
|
|
28
34
|
PromptModal: 'prompt-modal',
|
|
29
|
-
|
|
35
|
+
Toast: 'toast',
|
|
30
36
|
StartupCrash: 'startup-crash',
|
|
31
37
|
} as const;
|
|
32
38
|
|
|
33
39
|
export type UIComponent = ObjectValues<typeof UIComponents>;
|
|
34
40
|
|
|
35
|
-
export
|
|
41
|
+
export type ConfirmOptions = AcceptRefs<{
|
|
36
42
|
acceptText?: string;
|
|
43
|
+
acceptVariant?: ButtonVariant;
|
|
37
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;
|
|
38
59
|
}
|
|
39
60
|
|
|
40
|
-
export
|
|
61
|
+
export type PromptOptions = AcceptRefs<{
|
|
41
62
|
label?: string;
|
|
42
63
|
defaultValue?: string;
|
|
43
64
|
placeholder?: string;
|
|
44
65
|
acceptText?: string;
|
|
66
|
+
acceptVariant?: ButtonVariant;
|
|
45
67
|
cancelText?: string;
|
|
46
|
-
|
|
68
|
+
cancelVariant?: ButtonVariant;
|
|
69
|
+
trim?: boolean;
|
|
70
|
+
}>;
|
|
47
71
|
|
|
48
|
-
export interface
|
|
72
|
+
export interface ToastOptions {
|
|
49
73
|
component?: Component;
|
|
50
|
-
|
|
51
|
-
actions?:
|
|
74
|
+
variant?: ToastVariant;
|
|
75
|
+
actions?: ToastAction[];
|
|
52
76
|
}
|
|
53
77
|
|
|
54
78
|
export class UIService extends Service {
|
|
@@ -63,7 +87,7 @@ export class UIService extends Service {
|
|
|
63
87
|
public alert(message: string): void;
|
|
64
88
|
public alert(title: string, message: string): void;
|
|
65
89
|
public alert(messageOrTitle: string, message?: string): void {
|
|
66
|
-
const getProperties = ():
|
|
90
|
+
const getProperties = (): AlertModalProps => {
|
|
67
91
|
if (typeof message !== 'string') {
|
|
68
92
|
return { message: messageOrTitle };
|
|
69
93
|
}
|
|
@@ -74,38 +98,79 @@ export class UIService extends Service {
|
|
|
74
98
|
};
|
|
75
99
|
};
|
|
76
100
|
|
|
77
|
-
this.openModal(
|
|
101
|
+
this.openModal<ModalComponent<AlertModalProps>>(
|
|
102
|
+
this.requireComponent(UIComponents.AlertModal),
|
|
103
|
+
getProperties(),
|
|
104
|
+
);
|
|
78
105
|
}
|
|
79
106
|
|
|
107
|
+
/* eslint-disable max-len */
|
|
80
108
|
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
81
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
|
+
|
|
82
114
|
public async confirm(
|
|
83
115
|
messageOrTitle: string,
|
|
84
|
-
messageOrOptions?: string | ConfirmOptions,
|
|
85
|
-
options?: ConfirmOptions,
|
|
86
|
-
): Promise<boolean> {
|
|
87
|
-
const getProperties = ():
|
|
116
|
+
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
117
|
+
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
118
|
+
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
119
|
+
const getProperties = (): AcceptRefs<ConfirmModalProps> => {
|
|
88
120
|
if (typeof messageOrOptions !== 'string') {
|
|
89
121
|
return {
|
|
90
|
-
message: messageOrTitle,
|
|
91
122
|
...(messageOrOptions ?? {}),
|
|
123
|
+
message: messageOrTitle,
|
|
124
|
+
required: !!messageOrOptions?.required,
|
|
92
125
|
};
|
|
93
126
|
}
|
|
94
127
|
|
|
95
128
|
return {
|
|
129
|
+
...(options ?? {}),
|
|
96
130
|
title: messageOrTitle,
|
|
97
131
|
message: messageOrOptions,
|
|
98
|
-
|
|
132
|
+
required: !!options?.required,
|
|
99
133
|
};
|
|
100
134
|
};
|
|
101
135
|
|
|
102
|
-
|
|
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>(
|
|
103
143
|
this.requireComponent(UIComponents.ConfirmModal),
|
|
104
|
-
|
|
144
|
+
properties,
|
|
105
145
|
);
|
|
106
146
|
const result = await modal.beforeClose;
|
|
107
147
|
|
|
108
|
-
|
|
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;
|
|
109
174
|
}
|
|
110
175
|
|
|
111
176
|
public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
@@ -115,47 +180,65 @@ export class UIService extends Service {
|
|
|
115
180
|
messageOrOptions?: string | PromptOptions,
|
|
116
181
|
options?: PromptOptions,
|
|
117
182
|
): Promise<string | null> {
|
|
118
|
-
const
|
|
183
|
+
const trim = options?.trim ?? true;
|
|
184
|
+
const getProperties = (): PromptModalProps => {
|
|
119
185
|
if (typeof messageOrOptions !== 'string') {
|
|
120
186
|
return {
|
|
121
187
|
message: messageOrTitle,
|
|
122
188
|
...(messageOrOptions ?? {}),
|
|
123
|
-
};
|
|
189
|
+
} as PromptModalProps;
|
|
124
190
|
}
|
|
125
191
|
|
|
126
192
|
return {
|
|
127
193
|
title: messageOrTitle,
|
|
128
194
|
message: messageOrOptions,
|
|
129
195
|
...(options ?? {}),
|
|
130
|
-
};
|
|
196
|
+
} as PromptModalProps;
|
|
131
197
|
};
|
|
132
198
|
|
|
133
|
-
const modal = await this.openModal<ModalComponent<
|
|
199
|
+
const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
|
|
134
200
|
this.requireComponent(UIComponents.PromptModal),
|
|
135
201
|
getProperties(),
|
|
136
202
|
);
|
|
137
|
-
const
|
|
203
|
+
const rawResult = await modal.beforeClose;
|
|
204
|
+
const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
|
|
138
205
|
|
|
139
206
|
return result ?? null;
|
|
140
207
|
}
|
|
141
208
|
|
|
142
|
-
public async loading<T>(operation: Promise<T>): Promise<T>;
|
|
143
|
-
public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
|
|
144
|
-
public async loading<T>(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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) };
|
|
148
227
|
}
|
|
149
228
|
|
|
150
|
-
return {
|
|
229
|
+
return {
|
|
230
|
+
props: operationOrMessageOrOptions,
|
|
231
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
232
|
+
};
|
|
151
233
|
};
|
|
152
234
|
|
|
153
|
-
const
|
|
235
|
+
const { operationPromise, props } = processArgs();
|
|
236
|
+
const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
|
|
154
237
|
|
|
155
238
|
try {
|
|
156
|
-
|
|
239
|
+
const result = await operationPromise;
|
|
157
240
|
|
|
158
|
-
|
|
241
|
+
await after({ ms: 500 });
|
|
159
242
|
|
|
160
243
|
return result;
|
|
161
244
|
} finally {
|
|
@@ -163,23 +246,15 @@ export class UIService extends Service {
|
|
|
163
246
|
}
|
|
164
247
|
}
|
|
165
248
|
|
|
166
|
-
public
|
|
167
|
-
const
|
|
249
|
+
public toast(message: string, options: ToastOptions = {}): void {
|
|
250
|
+
const { component, ...otherOptions } = options;
|
|
251
|
+
const toast: UIToast = {
|
|
168
252
|
id: uuid(),
|
|
169
|
-
properties: { message, ...
|
|
170
|
-
component:
|
|
253
|
+
properties: { message, ...otherOptions },
|
|
254
|
+
component: markRaw(component ?? this.requireComponent(UIComponents.Toast)),
|
|
171
255
|
};
|
|
172
256
|
|
|
173
|
-
this.setState('
|
|
174
|
-
|
|
175
|
-
setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
public hideSnackbar(id: string): void {
|
|
179
|
-
this.setState(
|
|
180
|
-
'snackbars',
|
|
181
|
-
this.snackbars.filter((snackbar) => snackbar.id !== id),
|
|
182
|
-
);
|
|
257
|
+
this.setState('toasts', this.toasts.concat(toast));
|
|
183
258
|
}
|
|
184
259
|
|
|
185
260
|
public registerComponent(name: UIComponent, component: Component): void {
|
|
@@ -189,10 +264,10 @@ export class UIService extends Service {
|
|
|
189
264
|
public async openModal<TModalComponent extends ModalComponent>(
|
|
190
265
|
component: TModalComponent,
|
|
191
266
|
properties?: ModalProperties<TModalComponent>,
|
|
192
|
-
): Promise<
|
|
267
|
+
): Promise<UIModal<ModalResult<TModalComponent>>> {
|
|
193
268
|
const id = uuid();
|
|
194
269
|
const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
|
|
195
|
-
const modal:
|
|
270
|
+
const modal: UIModal<ModalResult<TModalComponent>> = {
|
|
196
271
|
id,
|
|
197
272
|
properties: properties ?? {},
|
|
198
273
|
component: markRaw(component),
|
|
@@ -217,12 +292,40 @@ export class UIService extends Service {
|
|
|
217
292
|
}
|
|
218
293
|
|
|
219
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
|
+
|
|
220
301
|
await Events.emit('close-modal', { id, result });
|
|
221
302
|
}
|
|
222
303
|
|
|
223
|
-
|
|
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> {
|
|
224
311
|
this.watchModalEvents();
|
|
225
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 }));
|
|
226
329
|
}
|
|
227
330
|
|
|
228
331
|
private watchModalEvents(): void {
|
|
@@ -234,31 +337,24 @@ export class UIService extends Service {
|
|
|
234
337
|
}
|
|
235
338
|
});
|
|
236
339
|
|
|
237
|
-
Events.on('modal-closed', async ({ modal, result }) => {
|
|
238
|
-
this.
|
|
239
|
-
'modals',
|
|
240
|
-
this.modals.filter((m) => m.id !== modal.id),
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
this.modalCallbacks[modal.id]?.closed?.(result);
|
|
244
|
-
|
|
245
|
-
delete this.modalCallbacks[modal.id];
|
|
246
|
-
|
|
247
|
-
const activeModal = this.modals.at(-1);
|
|
248
|
-
|
|
249
|
-
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
340
|
+
Events.on('modal-closed', async ({ modal: { id }, result }) => {
|
|
341
|
+
await this.removeModal(id, result);
|
|
250
342
|
});
|
|
251
343
|
}
|
|
252
344
|
|
|
253
345
|
private watchMountedEvent(): void {
|
|
254
346
|
Events.once('application-mounted', async () => {
|
|
255
|
-
|
|
347
|
+
if (!globalThis.document || !globalThis.getComputedStyle) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const splash = globalThis.document.getElementById('splash');
|
|
256
352
|
|
|
257
353
|
if (!splash) {
|
|
258
354
|
return;
|
|
259
355
|
}
|
|
260
356
|
|
|
261
|
-
if (
|
|
357
|
+
if (globalThis.getComputedStyle(splash).opacity !== '0') {
|
|
262
358
|
splash.style.opacity = '0';
|
|
263
359
|
|
|
264
360
|
await after({ ms: 600 });
|
|
@@ -268,16 +364,28 @@ export class UIService extends Service {
|
|
|
268
364
|
});
|
|
269
365
|
}
|
|
270
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
|
+
|
|
271
377
|
}
|
|
272
378
|
|
|
273
379
|
export default facade(UIService);
|
|
274
380
|
|
|
275
|
-
declare module '
|
|
381
|
+
declare module '@aerogel/core/services/Events' {
|
|
276
382
|
export interface EventsPayload {
|
|
277
|
-
'modal-will-close': { modal: Modal; result?: unknown };
|
|
278
|
-
'modal-closed': { modal: Modal; result?: unknown };
|
|
279
383
|
'close-modal': { id: string; result?: unknown };
|
|
280
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 };
|
|
281
388
|
'show-modal': { id: string };
|
|
389
|
+
'show-overlays-backdrop': void;
|
|
282
390
|
}
|
|
283
391
|
}
|
package/src/ui/index.ts
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
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 AGPromptModal from '../components/modals/AGPromptModal.vue';
|
|
12
|
-
import AGSnackbar from '../components/snackbars/AGSnackbar.vue';
|
|
13
|
-
import AGStartupCrash from '../components/lib/AGStartupCrash.vue';
|
|
14
14
|
import type { UIComponent } from './UI';
|
|
15
15
|
|
|
16
16
|
const services = { $ui: UI };
|
|
17
17
|
|
|
18
18
|
export * from './UI';
|
|
19
|
+
export * from './utils';
|
|
19
20
|
export { default as UI } from './UI';
|
|
20
21
|
|
|
21
22
|
export type UIServices = typeof services;
|
|
@@ -23,13 +24,13 @@ export type UIServices = typeof services;
|
|
|
23
24
|
export default definePlugin({
|
|
24
25
|
async install(app, options) {
|
|
25
26
|
const defaultComponents = {
|
|
26
|
-
[UIComponents.AlertModal]:
|
|
27
|
-
[UIComponents.ConfirmModal]:
|
|
28
|
-
[UIComponents.ErrorReportModal]:
|
|
29
|
-
[UIComponents.LoadingModal]:
|
|
30
|
-
[UIComponents.PromptModal]:
|
|
31
|
-
[UIComponents.
|
|
32
|
-
[UIComponents.StartupCrash]:
|
|
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,
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
Object.entries({
|
|
@@ -41,12 +42,12 @@ export default definePlugin({
|
|
|
41
42
|
},
|
|
42
43
|
});
|
|
43
44
|
|
|
44
|
-
declare module '
|
|
45
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
45
46
|
export interface AerogelOptions {
|
|
46
47
|
components?: Partial<Record<UIComponent, Component>>;
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
declare module '
|
|
51
|
+
declare module '@aerogel/core/services' {
|
|
51
52
|
export interface Services extends UIServices {}
|
|
52
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,13 +1,13 @@
|
|
|
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>(
|
|
@@ -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
|
+
}
|