@aerogel/core 0.0.0-next.bde642c4a8096c5fc3d5e676c2115da23f4bf1d8 → 0.0.0-next.c2e6acc000e97a1020c2e232678563c53884dd0e
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 +1396 -1645
- package/dist/aerogel-core.js +2968 -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} +6 -7
- package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
- package/src/components/headless/{forms/AGHeadlessInputInput.vue → HeadlessInputInput.vue} +11 -19
- package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
- package/src/components/headless/{forms/AGHeadlessInputTextArea.vue → HeadlessInputTextArea.vue} +7 -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 +5 -5
- package/src/components/lib/AGMeasured.vue +3 -2
- package/src/components/lib/AGStartupCrash.vue +8 -8
- package/src/components/lib/index.ts +0 -2
- package/src/components/snackbars/AGSnackbar.vue +10 -8
- 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/Markdown.vue +62 -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 +16 -0
- package/src/components/utils.ts +106 -9
- package/src/directives/index.ts +9 -5
- package/src/directives/measure.ts +25 -6
- 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} +33 -4
- package/src/forms/{Form.ts → FormController.ts} +46 -25
- package/src/forms/composition.ts +4 -4
- package/src/forms/index.ts +3 -2
- package/src/forms/utils.ts +22 -6
- package/src/forms/validation.ts +19 -0
- 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 +5 -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 +17 -5
- package/src/ui/UI.ts +168 -62
- package/src/ui/index.ts +17 -16
- 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 +18 -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 -33
- package/src/components/headless/forms/AGHeadlessInput.vue +0 -63
- 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/lib/AGMarkdown.vue +0 -41
- 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/ui/UI.ts
CHANGED
|
@@ -1,15 +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
|
-
import
|
|
17
|
+
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
18
|
+
import type { ModalComponent, Snackbar, UIModal } from './UI.state';
|
|
13
19
|
|
|
14
20
|
interface ModalCallbacks<T = unknown> {
|
|
15
21
|
willClose(result: T | undefined): void;
|
|
@@ -17,9 +23,8 @@ interface ModalCallbacks<T = unknown> {
|
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
|
|
20
|
-
type ModalResult<TComponent> =
|
|
21
|
-
? TResult
|
|
22
|
-
: never;
|
|
26
|
+
type ModalResult<TComponent> =
|
|
27
|
+
TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
|
|
23
28
|
|
|
24
29
|
export const UIComponents = {
|
|
25
30
|
AlertModal: 'alert-modal',
|
|
@@ -33,23 +38,36 @@ export const UIComponents = {
|
|
|
33
38
|
|
|
34
39
|
export type UIComponent = ObjectValues<typeof UIComponents>;
|
|
35
40
|
|
|
36
|
-
export
|
|
41
|
+
export type ConfirmOptions = AcceptRefs<{
|
|
37
42
|
acceptText?: string;
|
|
38
|
-
|
|
43
|
+
acceptVariant?: ButtonVariant;
|
|
39
44
|
cancelText?: string;
|
|
40
|
-
|
|
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;
|
|
41
59
|
}
|
|
42
60
|
|
|
43
|
-
export
|
|
61
|
+
export type PromptOptions = AcceptRefs<{
|
|
44
62
|
label?: string;
|
|
45
63
|
defaultValue?: string;
|
|
46
64
|
placeholder?: string;
|
|
47
65
|
acceptText?: string;
|
|
48
|
-
|
|
66
|
+
acceptVariant?: ButtonVariant;
|
|
49
67
|
cancelText?: string;
|
|
50
|
-
|
|
68
|
+
cancelVariant?: ButtonVariant;
|
|
51
69
|
trim?: boolean;
|
|
52
|
-
}
|
|
70
|
+
}>;
|
|
53
71
|
|
|
54
72
|
export interface ShowSnackbarOptions {
|
|
55
73
|
component?: Component;
|
|
@@ -69,7 +87,7 @@ export class UIService extends Service {
|
|
|
69
87
|
public alert(message: string): void;
|
|
70
88
|
public alert(title: string, message: string): void;
|
|
71
89
|
public alert(messageOrTitle: string, message?: string): void {
|
|
72
|
-
const getProperties = ():
|
|
90
|
+
const getProperties = (): AlertModalProps => {
|
|
73
91
|
if (typeof message !== 'string') {
|
|
74
92
|
return { message: messageOrTitle };
|
|
75
93
|
}
|
|
@@ -80,38 +98,79 @@ export class UIService extends Service {
|
|
|
80
98
|
};
|
|
81
99
|
};
|
|
82
100
|
|
|
83
|
-
this.openModal(
|
|
101
|
+
this.openModal<ModalComponent<AlertModalProps>>(
|
|
102
|
+
this.requireComponent(UIComponents.AlertModal),
|
|
103
|
+
getProperties(),
|
|
104
|
+
);
|
|
84
105
|
}
|
|
85
106
|
|
|
107
|
+
/* eslint-disable max-len */
|
|
86
108
|
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
87
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
|
+
|
|
88
114
|
public async confirm(
|
|
89
115
|
messageOrTitle: string,
|
|
90
|
-
messageOrOptions?: string | ConfirmOptions,
|
|
91
|
-
options?: ConfirmOptions,
|
|
92
|
-
): Promise<boolean> {
|
|
93
|
-
const getProperties = ():
|
|
116
|
+
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
117
|
+
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
118
|
+
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
119
|
+
const getProperties = (): AcceptRefs<ConfirmModalProps> => {
|
|
94
120
|
if (typeof messageOrOptions !== 'string') {
|
|
95
121
|
return {
|
|
96
|
-
message: messageOrTitle,
|
|
97
122
|
...(messageOrOptions ?? {}),
|
|
123
|
+
message: messageOrTitle,
|
|
124
|
+
required: !!messageOrOptions?.required,
|
|
98
125
|
};
|
|
99
126
|
}
|
|
100
127
|
|
|
101
128
|
return {
|
|
129
|
+
...(options ?? {}),
|
|
102
130
|
title: messageOrTitle,
|
|
103
131
|
message: messageOrOptions,
|
|
104
|
-
|
|
132
|
+
required: !!options?.required,
|
|
105
133
|
};
|
|
106
134
|
};
|
|
107
135
|
|
|
108
|
-
|
|
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>(
|
|
109
143
|
this.requireComponent(UIComponents.ConfirmModal),
|
|
110
|
-
|
|
144
|
+
properties,
|
|
111
145
|
);
|
|
112
146
|
const result = await modal.beforeClose;
|
|
113
147
|
|
|
114
|
-
|
|
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;
|
|
115
174
|
}
|
|
116
175
|
|
|
117
176
|
public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
@@ -122,22 +181,22 @@ export class UIService extends Service {
|
|
|
122
181
|
options?: PromptOptions,
|
|
123
182
|
): Promise<string | null> {
|
|
124
183
|
const trim = options?.trim ?? true;
|
|
125
|
-
const getProperties = ():
|
|
184
|
+
const getProperties = (): PromptModalProps => {
|
|
126
185
|
if (typeof messageOrOptions !== 'string') {
|
|
127
186
|
return {
|
|
128
187
|
message: messageOrTitle,
|
|
129
188
|
...(messageOrOptions ?? {}),
|
|
130
|
-
};
|
|
189
|
+
} as PromptModalProps;
|
|
131
190
|
}
|
|
132
191
|
|
|
133
192
|
return {
|
|
134
193
|
title: messageOrTitle,
|
|
135
194
|
message: messageOrOptions,
|
|
136
195
|
...(options ?? {}),
|
|
137
|
-
};
|
|
196
|
+
} as PromptModalProps;
|
|
138
197
|
};
|
|
139
198
|
|
|
140
|
-
const modal = await this.openModal<ModalComponent<
|
|
199
|
+
const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
|
|
141
200
|
this.requireComponent(UIComponents.PromptModal),
|
|
142
201
|
getProperties(),
|
|
143
202
|
);
|
|
@@ -147,23 +206,39 @@ export class UIService extends Service {
|
|
|
147
206
|
return result ?? null;
|
|
148
207
|
}
|
|
149
208
|
|
|
150
|
-
public async loading<T>(operation: Promise<T>): Promise<T>;
|
|
151
|
-
public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
|
|
152
|
-
public async loading<T>(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
+
};
|
|
156
223
|
}
|
|
157
224
|
|
|
158
|
-
|
|
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
|
+
};
|
|
159
233
|
};
|
|
160
234
|
|
|
161
|
-
const
|
|
235
|
+
const { operationPromise, props } = processArgs();
|
|
236
|
+
const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
|
|
162
237
|
|
|
163
238
|
try {
|
|
164
|
-
|
|
239
|
+
const result = await operationPromise;
|
|
165
240
|
|
|
166
|
-
|
|
241
|
+
await after({ ms: 500 });
|
|
167
242
|
|
|
168
243
|
return result;
|
|
169
244
|
} finally {
|
|
@@ -197,10 +272,10 @@ export class UIService extends Service {
|
|
|
197
272
|
public async openModal<TModalComponent extends ModalComponent>(
|
|
198
273
|
component: TModalComponent,
|
|
199
274
|
properties?: ModalProperties<TModalComponent>,
|
|
200
|
-
): Promise<
|
|
275
|
+
): Promise<UIModal<ModalResult<TModalComponent>>> {
|
|
201
276
|
const id = uuid();
|
|
202
277
|
const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
|
|
203
|
-
const modal:
|
|
278
|
+
const modal: UIModal<ModalResult<TModalComponent>> = {
|
|
204
279
|
id,
|
|
205
280
|
properties: properties ?? {},
|
|
206
281
|
component: markRaw(component),
|
|
@@ -225,12 +300,40 @@ export class UIService extends Service {
|
|
|
225
300
|
}
|
|
226
301
|
|
|
227
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
|
+
|
|
228
309
|
await Events.emit('close-modal', { id, result });
|
|
229
310
|
}
|
|
230
311
|
|
|
231
|
-
|
|
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> {
|
|
232
319
|
this.watchModalEvents();
|
|
233
320
|
this.watchMountedEvent();
|
|
321
|
+
this.watchViewportBreakpoints();
|
|
322
|
+
}
|
|
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 }));
|
|
234
337
|
}
|
|
235
338
|
|
|
236
339
|
private watchModalEvents(): void {
|
|
@@ -242,31 +345,24 @@ export class UIService extends Service {
|
|
|
242
345
|
}
|
|
243
346
|
});
|
|
244
347
|
|
|
245
|
-
Events.on('modal-closed', async ({ modal, result }) => {
|
|
246
|
-
this.
|
|
247
|
-
'modals',
|
|
248
|
-
this.modals.filter((m) => m.id !== modal.id),
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
this.modalCallbacks[modal.id]?.closed?.(result);
|
|
252
|
-
|
|
253
|
-
delete this.modalCallbacks[modal.id];
|
|
254
|
-
|
|
255
|
-
const activeModal = this.modals.at(-1);
|
|
256
|
-
|
|
257
|
-
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
348
|
+
Events.on('modal-closed', async ({ modal: { id }, result }) => {
|
|
349
|
+
await this.removeModal(id, result);
|
|
258
350
|
});
|
|
259
351
|
}
|
|
260
352
|
|
|
261
353
|
private watchMountedEvent(): void {
|
|
262
354
|
Events.once('application-mounted', async () => {
|
|
263
|
-
|
|
355
|
+
if (!globalThis.document || !globalThis.getComputedStyle) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const splash = globalThis.document.getElementById('splash');
|
|
264
360
|
|
|
265
361
|
if (!splash) {
|
|
266
362
|
return;
|
|
267
363
|
}
|
|
268
364
|
|
|
269
|
-
if (
|
|
365
|
+
if (globalThis.getComputedStyle(splash).opacity !== '0') {
|
|
270
366
|
splash.style.opacity = '0';
|
|
271
367
|
|
|
272
368
|
await after({ ms: 600 });
|
|
@@ -276,17 +372,27 @@ export class UIService extends Service {
|
|
|
276
372
|
});
|
|
277
373
|
}
|
|
278
374
|
|
|
375
|
+
private watchViewportBreakpoints(): void {
|
|
376
|
+
if (!globalThis.matchMedia) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
|
|
381
|
+
|
|
382
|
+
media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
|
|
383
|
+
}
|
|
384
|
+
|
|
279
385
|
}
|
|
280
386
|
|
|
281
387
|
export default facade(UIService);
|
|
282
388
|
|
|
283
|
-
declare module '
|
|
389
|
+
declare module '@aerogel/core/services/Events' {
|
|
284
390
|
export interface EventsPayload {
|
|
285
391
|
'close-modal': { id: string; result?: unknown };
|
|
286
392
|
'hide-modal': { id: string };
|
|
287
393
|
'hide-overlays-backdrop': void;
|
|
288
|
-
'modal-closed': { modal:
|
|
289
|
-
'modal-will-close': { modal:
|
|
394
|
+
'modal-closed': { modal: UIModal; result?: unknown };
|
|
395
|
+
'modal-will-close': { modal: UIModal; result?: unknown };
|
|
290
396
|
'show-modal': { id: string };
|
|
291
397
|
'show-overlays-backdrop': void;
|
|
292
398
|
}
|
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 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 };
|
|
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,11 +24,11 @@ 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]:
|
|
27
|
+
[UIComponents.AlertModal]: AlertModal,
|
|
28
|
+
[UIComponents.ConfirmModal]: ConfirmModal,
|
|
29
|
+
[UIComponents.ErrorReportModal]: ErrorReportModal,
|
|
30
|
+
[UIComponents.LoadingModal]: LoadingModal,
|
|
31
|
+
[UIComponents.PromptModal]: PromptModal,
|
|
31
32
|
[UIComponents.Snackbar]: AGSnackbar,
|
|
32
33
|
[UIComponents.StartupCrash]: AGStartupCrash,
|
|
33
34
|
};
|
|
@@ -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
|
+
}
|
|
@@ -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