@aerogel/core 0.0.0-next.b243de4e2590f02709edeebd8c13b74087592c04 → 0.0.0-next.b3caf219a503ce9b8c65ef1463132c9507f56c0a
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 +1346 -1647
- package/dist/aerogel-core.js +2960 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +27 -37
- package/src/bootstrap/bootstrap.test.ts +4 -7
- package/src/bootstrap/index.ts +25 -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 -5
- package/src/components/composition.ts +1 -1
- package/src/components/contracts/AlertModal.ts +4 -0
- package/src/components/contracts/Button.ts +15 -0
- package/src/components/contracts/ConfirmModal.ts +41 -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 +9 -0
- package/src/components/contracts/PromptModal.ts +28 -0
- package/src/components/contracts/index.ts +7 -0
- package/src/components/contracts/shared.ts +9 -0
- package/src/components/forms/AGSelect.vue +11 -17
- package/src/components/forms/index.ts +0 -4
- package/src/components/headless/HeadlessButton.vue +45 -0
- package/src/components/headless/HeadlessInput.vue +59 -0
- package/src/components/headless/{forms/AGHeadlessInputDescription.vue → HeadlessInputDescription.vue} +5 -6
- package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
- package/src/components/headless/{forms/AGHeadlessInputInput.vue → HeadlessInputInput.vue} +10 -19
- package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
- package/src/components/headless/{forms/AGHeadlessInputTextArea.vue → HeadlessInputTextArea.vue} +6 -9
- package/src/components/headless/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +16 -18
- 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/forms/AGHeadlessSelect.ts +3 -3
- package/src/components/headless/forms/AGHeadlessSelect.vue +16 -16
- package/src/components/headless/forms/AGHeadlessSelectError.vue +2 -2
- package/src/components/headless/forms/AGHeadlessSelectOption.vue +10 -18
- package/src/components/headless/forms/AGHeadlessSelectOptions.vue +19 -0
- package/src/components/headless/forms/AGHeadlessSelectTrigger.vue +25 -0
- package/src/components/headless/forms/composition.ts +2 -2
- package/src/components/headless/forms/index.ts +2 -12
- package/src/components/headless/index.ts +12 -1
- package/src/components/headless/snackbars/index.ts +3 -3
- package/src/components/index.ts +5 -5
- package/src/components/lib/AGErrorMessage.vue +3 -3
- package/src/components/lib/AGMarkdown.vue +16 -3
- package/src/components/lib/AGMeasured.vue +2 -2
- package/src/components/lib/AGStartupCrash.vue +6 -6
- package/src/components/lib/index.ts +0 -1
- package/src/components/snackbars/AGSnackbar.vue +8 -6
- package/src/components/ui/AlertModal.vue +13 -0
- package/src/components/ui/Button.vue +58 -0
- package/src/components/ui/Checkbox.vue +49 -0
- package/src/components/ui/ConfirmModal.vue +42 -0
- package/src/components/ui/ErrorReportModal.vue +62 -0
- package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +29 -20
- package/src/components/ui/ErrorReportModalTitle.vue +24 -0
- package/src/components/{forms/AGForm.vue → ui/Form.vue} +4 -5
- package/src/components/ui/Input.vue +52 -0
- package/src/components/ui/Link.vue +12 -0
- package/src/components/ui/LoadingModal.vue +32 -0
- package/src/components/ui/Modal.vue +55 -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/index.ts +15 -0
- package/src/components/utils.ts +106 -9
- package/src/directives/index.ts +9 -5
- package/src/directives/measure.ts +1 -1
- package/src/errors/Errors.state.ts +1 -1
- package/src/errors/Errors.ts +14 -14
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +9 -6
- package/src/errors/utils.ts +17 -1
- package/src/forms/{Form.test.ts → FormController.test.ts} +5 -4
- package/src/forms/{Form.ts → FormController.ts} +22 -19
- package/src/forms/composition.ts +4 -4
- package/src/forms/index.ts +2 -2
- package/src/forms/utils.ts +2 -2
- package/src/index.css +8 -0
- package/src/jobs/Job.ts +144 -2
- package/src/jobs/index.ts +4 -1
- package/src/jobs/listeners.ts +3 -0
- package/src/jobs/status.ts +4 -0
- package/src/lang/DefaultLangProvider.ts +7 -4
- package/src/lang/Lang.state.ts +1 -1
- package/src/lang/Lang.ts +1 -1
- 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 +13 -4
- package/src/services/App.ts +8 -3
- package/src/services/Cache.ts +1 -1
- package/src/services/Events.ts +15 -5
- package/src/services/Service.ts +116 -53
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +11 -5
- package/src/services/utils.ts +18 -0
- package/src/testing/index.ts +4 -3
- package/src/testing/setup.ts +5 -13
- package/src/ui/UI.state.ts +10 -5
- package/src/ui/UI.ts +145 -59
- package/src/ui/index.ts +16 -16
- 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/vue.ts +11 -13
- 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/forms/AGButton.vue +0 -44
- package/src/components/forms/AGCheckbox.vue +0 -41
- package/src/components/forms/AGInput.vue +0 -40
- package/src/components/headless/forms/AGHeadlessButton.ts +0 -3
- package/src/components/headless/forms/AGHeadlessButton.vue +0 -62
- package/src/components/headless/forms/AGHeadlessInput.ts +0 -34
- package/src/components/headless/forms/AGHeadlessInput.vue +0 -70
- package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
- 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/interfaces.ts +0 -24
- package/src/components/lib/AGLink.vue +0 -9
- package/src/components/modals/AGAlertModal.ts +0 -15
- package/src/components/modals/AGAlertModal.vue +0 -14
- package/src/components/modals/AGConfirmModal.ts +0 -33
- 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 -36
- package/src/components/modals/AGPromptModal.vue +0 -34
- package/src/components/modals/index.ts +0 -17
- 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 -17
- /package/src/components/{AGAppSnackbars.vue → AppSnackbars.vue} +0 -0
- /package/src/{main.ts → index.ts} +0 -0
package/src/testing/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { isTesting } from '@noeldemartin/utils';
|
|
1
2
|
import type { GetClosureArgs } from '@noeldemartin/utils';
|
|
2
3
|
|
|
3
|
-
import Events from '
|
|
4
|
-
import { definePlugin } from '
|
|
4
|
+
import Events from '@aerogel/core/services/Events';
|
|
5
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
5
6
|
|
|
6
7
|
export interface AerogelTestingRuntime {
|
|
7
8
|
on: (typeof Events)['on'];
|
|
@@ -9,7 +10,7 @@ export interface AerogelTestingRuntime {
|
|
|
9
10
|
|
|
10
11
|
export default definePlugin({
|
|
11
12
|
async install() {
|
|
12
|
-
if (
|
|
13
|
+
if (!isTesting()) {
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
15
16
|
|
package/src/testing/setup.ts
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FakeLocalStorage } from '@noeldemartin/testing';
|
|
2
2
|
import { beforeEach, vi } from 'vitest';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
global.jest = vi;
|
|
7
|
-
global.navigator = { languages: ['en'] };
|
|
8
|
-
global.localStorage = mock<Storage>({
|
|
9
|
-
getItem: () => null,
|
|
10
|
-
setItem: () => null,
|
|
11
|
-
});
|
|
4
|
+
vi.mock('dompurify', async () => {
|
|
5
|
+
return { default: { sanitize: (html: string) => html } };
|
|
12
6
|
});
|
|
13
7
|
|
|
14
8
|
beforeEach(() => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
getElementById: () => null,
|
|
18
|
-
});
|
|
9
|
+
FakeLocalStorage.reset();
|
|
10
|
+
FakeLocalStorage.patchGlobal();
|
|
19
11
|
});
|
package/src/ui/UI.state.ts
CHANGED
|
@@ -1,10 +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
6
|
|
|
7
|
-
export interface
|
|
7
|
+
export interface UIModal<T = unknown> {
|
|
8
8
|
id: string;
|
|
9
9
|
properties: Record<string, unknown>;
|
|
10
10
|
component: Component;
|
|
@@ -12,11 +12,16 @@ export interface Modal<T = unknown> {
|
|
|
12
12
|
afterClose: Promise<T | undefined>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export interface UIModalContext {
|
|
16
|
+
modal: UIModal;
|
|
17
|
+
childIndex?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
export interface ModalComponent<
|
|
16
21
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
17
|
-
Properties extends
|
|
22
|
+
Properties extends object = object,
|
|
18
23
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
19
|
-
Result = unknown
|
|
24
|
+
Result = unknown,
|
|
20
25
|
> {}
|
|
21
26
|
|
|
22
27
|
export interface Snackbar {
|
|
@@ -28,7 +33,7 @@ export interface Snackbar {
|
|
|
28
33
|
export default defineServiceState({
|
|
29
34
|
name: 'ui',
|
|
30
35
|
initialState: {
|
|
31
|
-
modals: [] as
|
|
36
|
+
modals: [] as UIModal[],
|
|
32
37
|
snackbars: [] as Snackbar[],
|
|
33
38
|
layout: getCurrentLayout(),
|
|
34
39
|
},
|
package/src/ui/UI.ts
CHANGED
|
@@ -1,16 +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 {
|
|
9
|
-
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 { SnackbarAction, SnackbarColor } from '@aerogel/core/components/headless/snackbars';
|
|
10
15
|
|
|
11
16
|
import Service from './UI.state';
|
|
12
17
|
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
13
|
-
import type {
|
|
18
|
+
import type { ModalComponent, Snackbar, UIModal } from './UI.state';
|
|
14
19
|
|
|
15
20
|
interface ModalCallbacks<T = unknown> {
|
|
16
21
|
willClose(result: T | undefined): void;
|
|
@@ -18,9 +23,8 @@ interface ModalCallbacks<T = unknown> {
|
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
|
|
21
|
-
type ModalResult<TComponent> =
|
|
22
|
-
? TResult
|
|
23
|
-
: never;
|
|
26
|
+
type ModalResult<TComponent> =
|
|
27
|
+
TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
|
|
24
28
|
|
|
25
29
|
export const UIComponents = {
|
|
26
30
|
AlertModal: 'alert-modal',
|
|
@@ -34,23 +38,36 @@ export const UIComponents = {
|
|
|
34
38
|
|
|
35
39
|
export type UIComponent = ObjectValues<typeof UIComponents>;
|
|
36
40
|
|
|
37
|
-
export
|
|
41
|
+
export type ConfirmOptions = AcceptRefs<{
|
|
38
42
|
acceptText?: string;
|
|
39
|
-
|
|
43
|
+
acceptVariant?: ButtonVariant;
|
|
40
44
|
cancelText?: string;
|
|
41
|
-
|
|
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;
|
|
42
59
|
}
|
|
43
60
|
|
|
44
|
-
export
|
|
61
|
+
export type PromptOptions = AcceptRefs<{
|
|
45
62
|
label?: string;
|
|
46
63
|
defaultValue?: string;
|
|
47
64
|
placeholder?: string;
|
|
48
65
|
acceptText?: string;
|
|
49
|
-
|
|
66
|
+
acceptVariant?: ButtonVariant;
|
|
50
67
|
cancelText?: string;
|
|
51
|
-
|
|
68
|
+
cancelVariant?: ButtonVariant;
|
|
52
69
|
trim?: boolean;
|
|
53
|
-
}
|
|
70
|
+
}>;
|
|
54
71
|
|
|
55
72
|
export interface ShowSnackbarOptions {
|
|
56
73
|
component?: Component;
|
|
@@ -70,7 +87,7 @@ export class UIService extends Service {
|
|
|
70
87
|
public alert(message: string): void;
|
|
71
88
|
public alert(title: string, message: string): void;
|
|
72
89
|
public alert(messageOrTitle: string, message?: string): void {
|
|
73
|
-
const getProperties = ():
|
|
90
|
+
const getProperties = (): AlertModalProps => {
|
|
74
91
|
if (typeof message !== 'string') {
|
|
75
92
|
return { message: messageOrTitle };
|
|
76
93
|
}
|
|
@@ -81,38 +98,79 @@ export class UIService extends Service {
|
|
|
81
98
|
};
|
|
82
99
|
};
|
|
83
100
|
|
|
84
|
-
this.openModal(
|
|
101
|
+
this.openModal<ModalComponent<AlertModalProps>>(
|
|
102
|
+
this.requireComponent(UIComponents.AlertModal),
|
|
103
|
+
getProperties(),
|
|
104
|
+
);
|
|
85
105
|
}
|
|
86
106
|
|
|
107
|
+
/* eslint-disable max-len */
|
|
87
108
|
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
88
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
|
+
|
|
89
114
|
public async confirm(
|
|
90
115
|
messageOrTitle: string,
|
|
91
|
-
messageOrOptions?: string | ConfirmOptions,
|
|
92
|
-
options?: ConfirmOptions,
|
|
93
|
-
): Promise<boolean> {
|
|
94
|
-
const getProperties = ():
|
|
116
|
+
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
117
|
+
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
118
|
+
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
119
|
+
const getProperties = (): AcceptRefs<ConfirmModalProps> => {
|
|
95
120
|
if (typeof messageOrOptions !== 'string') {
|
|
96
121
|
return {
|
|
97
|
-
message: messageOrTitle,
|
|
98
122
|
...(messageOrOptions ?? {}),
|
|
123
|
+
message: messageOrTitle,
|
|
124
|
+
required: !!messageOrOptions?.required,
|
|
99
125
|
};
|
|
100
126
|
}
|
|
101
127
|
|
|
102
128
|
return {
|
|
129
|
+
...(options ?? {}),
|
|
103
130
|
title: messageOrTitle,
|
|
104
131
|
message: messageOrOptions,
|
|
105
|
-
|
|
132
|
+
required: !!options?.required,
|
|
106
133
|
};
|
|
107
134
|
};
|
|
108
135
|
|
|
109
|
-
|
|
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>(
|
|
110
143
|
this.requireComponent(UIComponents.ConfirmModal),
|
|
111
|
-
|
|
144
|
+
properties,
|
|
112
145
|
);
|
|
113
146
|
const result = await modal.beforeClose;
|
|
114
147
|
|
|
115
|
-
|
|
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;
|
|
116
174
|
}
|
|
117
175
|
|
|
118
176
|
public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
@@ -123,22 +181,22 @@ export class UIService extends Service {
|
|
|
123
181
|
options?: PromptOptions,
|
|
124
182
|
): Promise<string | null> {
|
|
125
183
|
const trim = options?.trim ?? true;
|
|
126
|
-
const getProperties = ():
|
|
184
|
+
const getProperties = (): PromptModalProps => {
|
|
127
185
|
if (typeof messageOrOptions !== 'string') {
|
|
128
186
|
return {
|
|
129
187
|
message: messageOrTitle,
|
|
130
188
|
...(messageOrOptions ?? {}),
|
|
131
|
-
};
|
|
189
|
+
} as PromptModalProps;
|
|
132
190
|
}
|
|
133
191
|
|
|
134
192
|
return {
|
|
135
193
|
title: messageOrTitle,
|
|
136
194
|
message: messageOrOptions,
|
|
137
195
|
...(options ?? {}),
|
|
138
|
-
};
|
|
196
|
+
} as PromptModalProps;
|
|
139
197
|
};
|
|
140
198
|
|
|
141
|
-
const modal = await this.openModal<ModalComponent<
|
|
199
|
+
const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
|
|
142
200
|
this.requireComponent(UIComponents.PromptModal),
|
|
143
201
|
getProperties(),
|
|
144
202
|
);
|
|
@@ -150,25 +208,37 @@ export class UIService extends Service {
|
|
|
150
208
|
|
|
151
209
|
public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
|
|
152
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>;
|
|
153
212
|
public async loading<T>(
|
|
154
|
-
|
|
213
|
+
operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
|
|
155
214
|
operation?: Promise<T> | (() => T),
|
|
156
215
|
): Promise<T> {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
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) };
|
|
160
227
|
}
|
|
161
228
|
|
|
162
|
-
return {
|
|
229
|
+
return {
|
|
230
|
+
props: operationOrMessageOrOptions,
|
|
231
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
232
|
+
};
|
|
163
233
|
};
|
|
164
234
|
|
|
165
|
-
const
|
|
235
|
+
const { operationPromise, props } = processArgs();
|
|
236
|
+
const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
|
|
166
237
|
|
|
167
238
|
try {
|
|
168
|
-
|
|
169
|
-
operation = typeof operation === 'function' ? Promise.resolve(operation()) : operation;
|
|
239
|
+
const result = await operationPromise;
|
|
170
240
|
|
|
171
|
-
|
|
241
|
+
await after({ ms: 500 });
|
|
172
242
|
|
|
173
243
|
return result;
|
|
174
244
|
} finally {
|
|
@@ -202,10 +272,10 @@ export class UIService extends Service {
|
|
|
202
272
|
public async openModal<TModalComponent extends ModalComponent>(
|
|
203
273
|
component: TModalComponent,
|
|
204
274
|
properties?: ModalProperties<TModalComponent>,
|
|
205
|
-
): Promise<
|
|
275
|
+
): Promise<UIModal<ModalResult<TModalComponent>>> {
|
|
206
276
|
const id = uuid();
|
|
207
277
|
const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
|
|
208
|
-
const modal:
|
|
278
|
+
const modal: UIModal<ModalResult<TModalComponent>> = {
|
|
209
279
|
id,
|
|
210
280
|
properties: properties ?? {},
|
|
211
281
|
component: markRaw(component),
|
|
@@ -230,15 +300,42 @@ export class UIService extends Service {
|
|
|
230
300
|
}
|
|
231
301
|
|
|
232
302
|
public async closeModal(id: string, result?: unknown): Promise<void> {
|
|
303
|
+
if (!App.isMounted()) {
|
|
304
|
+
await this.removeModal(id, result);
|
|
305
|
+
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
233
309
|
await Events.emit('close-modal', { id, result });
|
|
234
310
|
}
|
|
235
311
|
|
|
236
|
-
|
|
312
|
+
public async closeAllModals(): Promise<void> {
|
|
313
|
+
while (this.modals.length > 0) {
|
|
314
|
+
await this.closeModal(required(this.modals[this.modals.length - 1]).id);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
protected override async boot(): Promise<void> {
|
|
237
319
|
this.watchModalEvents();
|
|
238
320
|
this.watchMountedEvent();
|
|
239
321
|
this.watchViewportBreakpoints();
|
|
240
322
|
}
|
|
241
323
|
|
|
324
|
+
private async removeModal(id: string, result?: unknown): Promise<void> {
|
|
325
|
+
this.setState(
|
|
326
|
+
'modals',
|
|
327
|
+
this.modals.filter((m) => m.id !== id),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
this.modalCallbacks[id]?.closed?.(result);
|
|
331
|
+
|
|
332
|
+
delete this.modalCallbacks[id];
|
|
333
|
+
|
|
334
|
+
const activeModal = this.modals.at(-1);
|
|
335
|
+
|
|
336
|
+
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
337
|
+
}
|
|
338
|
+
|
|
242
339
|
private watchModalEvents(): void {
|
|
243
340
|
Events.on('modal-will-close', ({ modal, result }) => {
|
|
244
341
|
this.modalCallbacks[modal.id]?.willClose?.(result);
|
|
@@ -248,19 +345,8 @@ export class UIService extends Service {
|
|
|
248
345
|
}
|
|
249
346
|
});
|
|
250
347
|
|
|
251
|
-
Events.on('modal-closed', async ({ modal, result }) => {
|
|
252
|
-
this.
|
|
253
|
-
'modals',
|
|
254
|
-
this.modals.filter((m) => m.id !== modal.id),
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
this.modalCallbacks[modal.id]?.closed?.(result);
|
|
258
|
-
|
|
259
|
-
delete this.modalCallbacks[modal.id];
|
|
260
|
-
|
|
261
|
-
const activeModal = this.modals.at(-1);
|
|
262
|
-
|
|
263
|
-
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
348
|
+
Events.on('modal-closed', async ({ modal: { id }, result }) => {
|
|
349
|
+
await this.removeModal(id, result);
|
|
264
350
|
});
|
|
265
351
|
}
|
|
266
352
|
|
|
@@ -300,13 +386,13 @@ export class UIService extends Service {
|
|
|
300
386
|
|
|
301
387
|
export default facade(UIService);
|
|
302
388
|
|
|
303
|
-
declare module '
|
|
389
|
+
declare module '@aerogel/core/services/Events' {
|
|
304
390
|
export interface EventsPayload {
|
|
305
391
|
'close-modal': { id: string; result?: unknown };
|
|
306
392
|
'hide-modal': { id: string };
|
|
307
393
|
'hide-overlays-backdrop': void;
|
|
308
|
-
'modal-closed': { modal:
|
|
309
|
-
'modal-will-close': { modal:
|
|
394
|
+
'modal-closed': { modal: UIModal; result?: unknown };
|
|
395
|
+
'modal-will-close': { modal: UIModal; result?: unknown };
|
|
310
396
|
'show-modal': { id: string };
|
|
311
397
|
'show-overlays-backdrop': void;
|
|
312
398
|
}
|
package/src/ui/index.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { Component } from 'vue';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import AGSnackbar from '@aerogel/core/components/snackbars/AGSnackbar.vue';
|
|
4
|
+
import AGStartupCrash from '@aerogel/core/components/lib/AGStartupCrash.vue';
|
|
5
|
+
import AlertModal from '@aerogel/core/components/ui/AlertModal.vue';
|
|
6
|
+
import ConfirmModal from '@aerogel/core/components/ui/ConfirmModal.vue';
|
|
7
|
+
import ErrorReportModal from '@aerogel/core/components/ui/ErrorReportModal.vue';
|
|
8
|
+
import LoadingModal from '@aerogel/core/components/ui/LoadingModal.vue';
|
|
9
|
+
import PromptModal from '@aerogel/core/components/ui/PromptModal.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 };
|
|
@@ -24,11 +24,11 @@ export type UIServices = typeof services;
|
|
|
24
24
|
export default definePlugin({
|
|
25
25
|
async install(app, options) {
|
|
26
26
|
const defaultComponents = {
|
|
27
|
-
[UIComponents.AlertModal]:
|
|
28
|
-
[UIComponents.ConfirmModal]:
|
|
29
|
-
[UIComponents.ErrorReportModal]:
|
|
30
|
-
[UIComponents.LoadingModal]:
|
|
31
|
-
[UIComponents.PromptModal]:
|
|
27
|
+
[UIComponents.AlertModal]: AlertModal,
|
|
28
|
+
[UIComponents.ConfirmModal]: ConfirmModal,
|
|
29
|
+
[UIComponents.ErrorReportModal]: ErrorReportModal,
|
|
30
|
+
[UIComponents.LoadingModal]: LoadingModal,
|
|
31
|
+
[UIComponents.PromptModal]: PromptModal,
|
|
32
32
|
[UIComponents.Snackbar]: AGSnackbar,
|
|
33
33
|
[UIComponents.StartupCrash]: AGStartupCrash,
|
|
34
34
|
};
|
|
@@ -42,12 +42,12 @@ export default definePlugin({
|
|
|
42
42
|
},
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
declare module '
|
|
45
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
46
46
|
export interface AerogelOptions {
|
|
47
47
|
components?: Partial<Record<UIComponent, Component>>;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
declare module '
|
|
51
|
+
declare module '@aerogel/core/services' {
|
|
52
52
|
export interface Services extends UIServices {}
|
|
53
53
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { after } from '@noeldemartin/utils';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { ref } from 'vue';
|
|
4
|
+
|
|
5
|
+
import { computedDebounce } from './state';
|
|
6
|
+
|
|
7
|
+
describe('Vue state helpers', () => {
|
|
8
|
+
|
|
9
|
+
it('computes debounced state', async () => {
|
|
10
|
+
// Initial
|
|
11
|
+
const state = ref(0);
|
|
12
|
+
const value = computedDebounce({ delay: 90 }, () => state.value);
|
|
13
|
+
|
|
14
|
+
expect(value.value).toBe(null);
|
|
15
|
+
|
|
16
|
+
await after({ ms: 100 });
|
|
17
|
+
|
|
18
|
+
expect(value.value).toBe(0);
|
|
19
|
+
|
|
20
|
+
// Update
|
|
21
|
+
state.value = 42;
|
|
22
|
+
|
|
23
|
+
expect(value.value).toBe(0);
|
|
24
|
+
|
|
25
|
+
await after({ ms: 100 });
|
|
26
|
+
|
|
27
|
+
expect(value.value).toBe(42);
|
|
28
|
+
|
|
29
|
+
// Debounced Update
|
|
30
|
+
state.value = 23;
|
|
31
|
+
|
|
32
|
+
expect(value.value).toBe(42);
|
|
33
|
+
|
|
34
|
+
await after({ ms: 50 });
|
|
35
|
+
|
|
36
|
+
state.value = 32;
|
|
37
|
+
|
|
38
|
+
await after({ ms: 50 });
|
|
39
|
+
|
|
40
|
+
expect(value.value).toBe(42);
|
|
41
|
+
|
|
42
|
+
await after({ ms: 100 });
|
|
43
|
+
|
|
44
|
+
expect(value.value).toBe(32);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { debounce } from '@noeldemartin/utils';
|
|
2
|
+
import { ref, watchEffect } from 'vue';
|
|
3
|
+
import type { ComputedGetter, ComputedRef } from '@vue/runtime-core';
|
|
4
|
+
|
|
5
|
+
export interface ComputedDebounceOptions<T> {
|
|
6
|
+
initial?: T;
|
|
7
|
+
delay?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function computedDebounce<T>(options: ComputedDebounceOptions<T>, getter: ComputedGetter<T>): ComputedRef<T>;
|
|
11
|
+
export function computedDebounce<T>(getter: ComputedGetter<T>): ComputedRef<T | null>;
|
|
12
|
+
export function computedDebounce<T>(
|
|
13
|
+
optionsOrGetter: ComputedGetter<T> | ComputedDebounceOptions<T>,
|
|
14
|
+
inputGetter?: ComputedGetter<T>,
|
|
15
|
+
): ComputedRef<T> {
|
|
16
|
+
const inputOptions = inputGetter ? (optionsOrGetter as ComputedDebounceOptions<T>) : {};
|
|
17
|
+
const getter = inputGetter ?? (optionsOrGetter as ComputedGetter<T>);
|
|
18
|
+
const state = ref(inputOptions.initial ?? null);
|
|
19
|
+
const update = debounce((value) => (state.value = value), inputOptions.delay ?? 300);
|
|
20
|
+
|
|
21
|
+
watchEffect(() => update(getter()));
|
|
22
|
+
|
|
23
|
+
return state as unknown as ComputedRef<T>;
|
|
24
|
+
}
|
package/src/utils/index.ts
CHANGED