@aerogel/core 0.0.0-next.f9394854509d71d644498ac087706a2f8f8eea1c → 0.0.0-next.fcfbfdc3428c34c4d1c0e781b61d244f13232fc9
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 +2418 -1771
- package/dist/aerogel-core.js +3266 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +30 -37
- package/src/bootstrap/bootstrap.test.ts +4 -7
- package/src/bootstrap/index.ts +21 -19
- 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/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 +20 -0
- package/src/components/contracts/ErrorReportModal.ts +29 -0
- package/src/components/contracts/Input.ts +26 -0
- package/src/components/contracts/LoadingModal.ts +22 -0
- package/src/components/contracts/Modal.ts +21 -0
- package/src/components/contracts/PromptModal.ts +30 -0
- package/src/components/contracts/Select.ts +44 -0
- package/src/components/contracts/Toast.ts +13 -0
- package/src/components/contracts/index.ts +11 -0
- package/src/components/headless/HeadlessButton.vue +51 -0
- package/src/components/headless/HeadlessInput.vue +59 -0
- package/src/components/headless/{forms/AGHeadlessInputDescription.vue → HeadlessInputDescription.vue} +7 -8
- package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
- package/src/components/headless/{forms/AGHeadlessInputInput.vue → HeadlessInputInput.vue} +16 -25
- package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
- package/src/components/headless/{forms/AGHeadlessInputTextArea.vue → HeadlessInputTextArea.vue} +10 -13
- package/src/components/headless/HeadlessModal.vue +92 -0
- package/src/components/headless/HeadlessModalContent.vue +30 -0
- package/src/components/headless/HeadlessModalDescription.vue +12 -0
- package/src/components/headless/HeadlessModalOverlay.vue +12 -0
- package/src/components/headless/HeadlessModalTitle.vue +12 -0
- package/src/components/headless/HeadlessSelect.vue +118 -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 +37 -0
- package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
- package/src/components/headless/HeadlessSelectValue.vue +18 -0
- package/src/components/headless/HeadlessToast.vue +18 -0
- package/src/components/headless/HeadlessToastAction.vue +13 -0
- package/src/components/headless/index.ts +19 -3
- package/src/components/index.ts +6 -11
- 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 +32 -0
- package/src/components/ui/DropdownMenuOption.vue +14 -0
- package/src/components/ui/DropdownMenuOptions.vue +27 -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/{forms/AGForm.vue → ui/Form.vue} +4 -5
- 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 +91 -0
- package/src/components/ui/ModalContext.vue +30 -0
- package/src/components/ui/ProgressBar.vue +51 -0
- package/src/components/ui/PromptModal.vue +35 -0
- package/src/components/ui/Select.vue +25 -0
- package/src/components/ui/SelectLabel.vue +17 -0
- package/src/components/ui/SelectOption.vue +29 -0
- package/src/components/ui/SelectOptions.vue +30 -0
- package/src/components/ui/SelectTrigger.vue +29 -0
- package/src/components/ui/SettingsModal.vue +15 -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 +30 -0
- 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 +17 -18
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +9 -6
- package/src/errors/utils.ts +1 -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 +54 -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 +12 -6
- package/src/lang/settings/Language.vue +48 -0
- package/src/lang/settings/index.ts +10 -0
- package/src/plugins/Plugin.ts +1 -1
- package/src/plugins/index.ts +10 -7
- package/src/services/App.state.ts +23 -4
- package/src/services/App.ts +16 -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 +14 -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 +12 -7
- package/src/ui/UI.ts +161 -78
- package/src/ui/index.ts +18 -18
- package/src/utils/classes.ts +49 -0
- package/src/utils/composition/events.ts +2 -2
- package/src/utils/composition/forms.ts +14 -4
- package/src/utils/composition/persistent.test.ts +33 -0
- package/src/utils/composition/persistent.ts +11 -0
- package/src/utils/composition/state.test.ts +47 -0
- package/src/utils/composition/state.ts +33 -0
- package/src/utils/index.ts +5 -1
- package/src/utils/markdown.test.ts +50 -0
- package/src/utils/markdown.ts +19 -6
- package/src/utils/types.ts +3 -0
- package/src/utils/vue.ts +28 -136
- 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/composition.ts +0 -23
- 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/AGInput.vue +0 -40
- 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.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/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/composition.ts +0 -10
- package/src/components/headless/forms/index.ts +0 -18
- package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
- package/src/components/headless/modals/AGHeadlessModal.vue +0 -86
- 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/interfaces.ts +0 -24
- package/src/components/lib/AGErrorMessage.vue +0 -16
- package/src/components/lib/AGLink.vue +0 -9
- package/src/components/lib/AGMarkdown.vue +0 -41
- package/src/components/lib/AGMeasured.vue +0 -16
- 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 -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/components/snackbars/AGSnackbar.vue +0 -36
- package/src/components/snackbars/index.ts +0 -3
- package/src/components/utils.ts +0 -10
- package/src/directives/initial-focus.ts +0 -11
- package/src/main.histoire.ts +0 -1
- package/src/utils/tailwindcss.test.ts +0 -26
- package/src/utils/tailwindcss.ts +0 -7
- package/tailwind.config.js +0 -4
- package/tsconfig.json +0 -11
- package/vite.config.ts +0 -17
- /package/src/{main.ts → index.ts} +0 -0
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 { ToastAction, ToastVariant } from '@aerogel/core/components/contracts/Toast';
|
|
10
15
|
|
|
11
16
|
import Service from './UI.state';
|
|
12
17
|
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
13
|
-
import type {
|
|
18
|
+
import type { ModalComponent, UIModal, UIToast } 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',
|
|
@@ -28,34 +32,48 @@ export const UIComponents = {
|
|
|
28
32
|
ErrorReportModal: 'error-report-modal',
|
|
29
33
|
LoadingModal: 'loading-modal',
|
|
30
34
|
PromptModal: 'prompt-modal',
|
|
31
|
-
|
|
35
|
+
Toast: 'toast',
|
|
32
36
|
StartupCrash: 'startup-crash',
|
|
37
|
+
RouterLink: 'router-link',
|
|
33
38
|
} as const;
|
|
34
39
|
|
|
35
40
|
export type UIComponent = ObjectValues<typeof UIComponents>;
|
|
36
41
|
|
|
37
|
-
export
|
|
42
|
+
export type ConfirmOptions = AcceptRefs<{
|
|
38
43
|
acceptText?: string;
|
|
39
|
-
|
|
44
|
+
acceptVariant?: ButtonVariant;
|
|
40
45
|
cancelText?: string;
|
|
41
|
-
|
|
46
|
+
cancelVariant?: ButtonVariant;
|
|
47
|
+
actions?: Record<string, () => unknown>;
|
|
48
|
+
required?: boolean;
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
export type LoadingOptions = AcceptRefs<{
|
|
52
|
+
title?: string;
|
|
53
|
+
message?: string;
|
|
54
|
+
progress?: number;
|
|
55
|
+
}>;
|
|
56
|
+
|
|
57
|
+
export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
|
|
58
|
+
extends ConfirmOptions {
|
|
59
|
+
checkboxes?: T;
|
|
42
60
|
}
|
|
43
61
|
|
|
44
|
-
export
|
|
62
|
+
export type PromptOptions = AcceptRefs<{
|
|
45
63
|
label?: string;
|
|
46
64
|
defaultValue?: string;
|
|
47
65
|
placeholder?: string;
|
|
48
66
|
acceptText?: string;
|
|
49
|
-
|
|
67
|
+
acceptVariant?: ButtonVariant;
|
|
50
68
|
cancelText?: string;
|
|
51
|
-
|
|
69
|
+
cancelVariant?: ButtonVariant;
|
|
52
70
|
trim?: boolean;
|
|
53
|
-
}
|
|
71
|
+
}>;
|
|
54
72
|
|
|
55
|
-
export interface
|
|
73
|
+
export interface ToastOptions {
|
|
56
74
|
component?: Component;
|
|
57
|
-
|
|
58
|
-
actions?:
|
|
75
|
+
variant?: ToastVariant;
|
|
76
|
+
actions?: ToastAction[];
|
|
59
77
|
}
|
|
60
78
|
|
|
61
79
|
export class UIService extends Service {
|
|
@@ -63,14 +81,18 @@ export class UIService extends Service {
|
|
|
63
81
|
private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
|
|
64
82
|
private components: Partial<Record<UIComponent, Component>> = {};
|
|
65
83
|
|
|
84
|
+
public resolveComponent(name: UIComponent): Component | null {
|
|
85
|
+
return this.components[name] ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
66
88
|
public requireComponent(name: UIComponent): Component {
|
|
67
|
-
return this.
|
|
89
|
+
return this.resolveComponent(name) ?? fail(`UI Component '${name}' is not defined!`);
|
|
68
90
|
}
|
|
69
91
|
|
|
70
92
|
public alert(message: string): void;
|
|
71
93
|
public alert(title: string, message: string): void;
|
|
72
94
|
public alert(messageOrTitle: string, message?: string): void {
|
|
73
|
-
const getProperties = ():
|
|
95
|
+
const getProperties = (): AlertModalProps => {
|
|
74
96
|
if (typeof message !== 'string') {
|
|
75
97
|
return { message: messageOrTitle };
|
|
76
98
|
}
|
|
@@ -81,38 +103,79 @@ export class UIService extends Service {
|
|
|
81
103
|
};
|
|
82
104
|
};
|
|
83
105
|
|
|
84
|
-
this.openModal(
|
|
106
|
+
this.openModal<ModalComponent<AlertModalProps>>(
|
|
107
|
+
this.requireComponent(UIComponents.AlertModal),
|
|
108
|
+
getProperties(),
|
|
109
|
+
);
|
|
85
110
|
}
|
|
86
111
|
|
|
112
|
+
/* eslint-disable max-len */
|
|
87
113
|
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
88
114
|
public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
115
|
+
public async confirm<T extends ConfirmModalCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
116
|
+
public async confirm<T extends ConfirmModalCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
117
|
+
/* eslint-enable max-len */
|
|
118
|
+
|
|
89
119
|
public async confirm(
|
|
90
120
|
messageOrTitle: string,
|
|
91
|
-
messageOrOptions?: string | ConfirmOptions,
|
|
92
|
-
options?: ConfirmOptions,
|
|
93
|
-
): Promise<boolean> {
|
|
94
|
-
const getProperties = ():
|
|
121
|
+
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
122
|
+
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
123
|
+
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
124
|
+
const getProperties = (): AcceptRefs<ConfirmModalProps> => {
|
|
95
125
|
if (typeof messageOrOptions !== 'string') {
|
|
96
126
|
return {
|
|
97
|
-
message: messageOrTitle,
|
|
98
127
|
...(messageOrOptions ?? {}),
|
|
128
|
+
message: messageOrTitle,
|
|
129
|
+
required: !!messageOrOptions?.required,
|
|
99
130
|
};
|
|
100
131
|
}
|
|
101
132
|
|
|
102
133
|
return {
|
|
134
|
+
...(options ?? {}),
|
|
103
135
|
title: messageOrTitle,
|
|
104
136
|
message: messageOrOptions,
|
|
105
|
-
|
|
137
|
+
required: !!options?.required,
|
|
106
138
|
};
|
|
107
139
|
};
|
|
108
140
|
|
|
109
|
-
|
|
141
|
+
type ConfirmModalComponent = ModalComponent<
|
|
142
|
+
AcceptRefs<ConfirmModalProps>,
|
|
143
|
+
boolean | [boolean, Record<string, boolean>]
|
|
144
|
+
>;
|
|
145
|
+
|
|
146
|
+
const properties = getProperties();
|
|
147
|
+
const modal = await this.openModal<ConfirmModalComponent>(
|
|
110
148
|
this.requireComponent(UIComponents.ConfirmModal),
|
|
111
|
-
|
|
149
|
+
properties,
|
|
112
150
|
);
|
|
113
151
|
const result = await modal.beforeClose;
|
|
114
152
|
|
|
115
|
-
|
|
153
|
+
const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
|
|
154
|
+
const checkboxes =
|
|
155
|
+
typeof result === 'object'
|
|
156
|
+
? result[1]
|
|
157
|
+
: Object.entries(properties.checkboxes ?? {}).reduce(
|
|
158
|
+
(values, [checkbox, { default: defaultValue }]) => ({
|
|
159
|
+
[checkbox]: defaultValue ?? false,
|
|
160
|
+
...values,
|
|
161
|
+
}),
|
|
162
|
+
{} as Record<string, boolean>,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
|
|
166
|
+
if (!checkbox.required || checkboxes[name]) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (confirmed && isDevelopment()) {
|
|
171
|
+
// eslint-disable-next-line no-console
|
|
172
|
+
console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return [false, checkboxes];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
|
|
116
179
|
}
|
|
117
180
|
|
|
118
181
|
public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
@@ -123,22 +186,22 @@ export class UIService extends Service {
|
|
|
123
186
|
options?: PromptOptions,
|
|
124
187
|
): Promise<string | null> {
|
|
125
188
|
const trim = options?.trim ?? true;
|
|
126
|
-
const getProperties = ():
|
|
189
|
+
const getProperties = (): PromptModalProps => {
|
|
127
190
|
if (typeof messageOrOptions !== 'string') {
|
|
128
191
|
return {
|
|
129
192
|
message: messageOrTitle,
|
|
130
193
|
...(messageOrOptions ?? {}),
|
|
131
|
-
};
|
|
194
|
+
} as PromptModalProps;
|
|
132
195
|
}
|
|
133
196
|
|
|
134
197
|
return {
|
|
135
198
|
title: messageOrTitle,
|
|
136
199
|
message: messageOrOptions,
|
|
137
200
|
...(options ?? {}),
|
|
138
|
-
};
|
|
201
|
+
} as PromptModalProps;
|
|
139
202
|
};
|
|
140
203
|
|
|
141
|
-
const modal = await this.openModal<ModalComponent<
|
|
204
|
+
const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
|
|
142
205
|
this.requireComponent(UIComponents.PromptModal),
|
|
143
206
|
getProperties(),
|
|
144
207
|
);
|
|
@@ -150,25 +213,37 @@ export class UIService extends Service {
|
|
|
150
213
|
|
|
151
214
|
public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
|
|
152
215
|
public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
|
|
216
|
+
public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
|
|
153
217
|
public async loading<T>(
|
|
154
|
-
|
|
218
|
+
operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
|
|
155
219
|
operation?: Promise<T> | (() => T),
|
|
156
220
|
): Promise<T> {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
221
|
+
const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
|
|
222
|
+
const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
|
|
223
|
+
if (typeof operationOrMessageOrOptions === 'string') {
|
|
224
|
+
return {
|
|
225
|
+
props: { message: operationOrMessageOrOptions },
|
|
226
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
|
|
231
|
+
return { operationPromise: processOperation(operationOrMessageOrOptions) };
|
|
160
232
|
}
|
|
161
233
|
|
|
162
|
-
return {
|
|
234
|
+
return {
|
|
235
|
+
props: operationOrMessageOrOptions,
|
|
236
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
237
|
+
};
|
|
163
238
|
};
|
|
164
239
|
|
|
165
|
-
const
|
|
240
|
+
const { operationPromise, props } = processArgs();
|
|
241
|
+
const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
|
|
166
242
|
|
|
167
243
|
try {
|
|
168
|
-
|
|
169
|
-
operation = typeof operation === 'function' ? Promise.resolve(operation()) : operation;
|
|
244
|
+
const result = await operationPromise;
|
|
170
245
|
|
|
171
|
-
|
|
246
|
+
await after({ ms: 500 });
|
|
172
247
|
|
|
173
248
|
return result;
|
|
174
249
|
} finally {
|
|
@@ -176,23 +251,15 @@ export class UIService extends Service {
|
|
|
176
251
|
}
|
|
177
252
|
}
|
|
178
253
|
|
|
179
|
-
public
|
|
180
|
-
const
|
|
254
|
+
public toast(message: string, options: ToastOptions = {}): void {
|
|
255
|
+
const { component, ...otherOptions } = options;
|
|
256
|
+
const toast: UIToast = {
|
|
181
257
|
id: uuid(),
|
|
182
|
-
properties: { message, ...
|
|
183
|
-
component: markRaw(
|
|
258
|
+
properties: { message, ...otherOptions },
|
|
259
|
+
component: markRaw(component ?? this.requireComponent(UIComponents.Toast)),
|
|
184
260
|
};
|
|
185
261
|
|
|
186
|
-
this.setState('
|
|
187
|
-
|
|
188
|
-
setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
public hideSnackbar(id: string): void {
|
|
192
|
-
this.setState(
|
|
193
|
-
'snackbars',
|
|
194
|
-
this.snackbars.filter((snackbar) => snackbar.id !== id),
|
|
195
|
-
);
|
|
262
|
+
this.setState('toasts', this.toasts.concat(toast));
|
|
196
263
|
}
|
|
197
264
|
|
|
198
265
|
public registerComponent(name: UIComponent, component: Component): void {
|
|
@@ -202,10 +269,10 @@ export class UIService extends Service {
|
|
|
202
269
|
public async openModal<TModalComponent extends ModalComponent>(
|
|
203
270
|
component: TModalComponent,
|
|
204
271
|
properties?: ModalProperties<TModalComponent>,
|
|
205
|
-
): Promise<
|
|
272
|
+
): Promise<UIModal<ModalResult<TModalComponent>>> {
|
|
206
273
|
const id = uuid();
|
|
207
274
|
const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
|
|
208
|
-
const modal:
|
|
275
|
+
const modal: UIModal<ModalResult<TModalComponent>> = {
|
|
209
276
|
id,
|
|
210
277
|
properties: properties ?? {},
|
|
211
278
|
component: markRaw(component),
|
|
@@ -230,15 +297,42 @@ export class UIService extends Service {
|
|
|
230
297
|
}
|
|
231
298
|
|
|
232
299
|
public async closeModal(id: string, result?: unknown): Promise<void> {
|
|
300
|
+
if (!App.isMounted()) {
|
|
301
|
+
await this.removeModal(id, result);
|
|
302
|
+
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
233
306
|
await Events.emit('close-modal', { id, result });
|
|
234
307
|
}
|
|
235
308
|
|
|
236
|
-
|
|
309
|
+
public async closeAllModals(): Promise<void> {
|
|
310
|
+
while (this.modals.length > 0) {
|
|
311
|
+
await this.closeModal(required(this.modals[this.modals.length - 1]).id);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
protected override async boot(): Promise<void> {
|
|
237
316
|
this.watchModalEvents();
|
|
238
317
|
this.watchMountedEvent();
|
|
239
318
|
this.watchViewportBreakpoints();
|
|
240
319
|
}
|
|
241
320
|
|
|
321
|
+
private async removeModal(id: string, result?: unknown): Promise<void> {
|
|
322
|
+
this.setState(
|
|
323
|
+
'modals',
|
|
324
|
+
this.modals.filter((m) => m.id !== id),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
this.modalCallbacks[id]?.closed?.(result);
|
|
328
|
+
|
|
329
|
+
delete this.modalCallbacks[id];
|
|
330
|
+
|
|
331
|
+
const activeModal = this.modals.at(-1);
|
|
332
|
+
|
|
333
|
+
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
334
|
+
}
|
|
335
|
+
|
|
242
336
|
private watchModalEvents(): void {
|
|
243
337
|
Events.on('modal-will-close', ({ modal, result }) => {
|
|
244
338
|
this.modalCallbacks[modal.id]?.willClose?.(result);
|
|
@@ -248,19 +342,8 @@ export class UIService extends Service {
|
|
|
248
342
|
}
|
|
249
343
|
});
|
|
250
344
|
|
|
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 }));
|
|
345
|
+
Events.on('modal-closed', async ({ modal: { id }, result }) => {
|
|
346
|
+
await this.removeModal(id, result);
|
|
264
347
|
});
|
|
265
348
|
}
|
|
266
349
|
|
|
@@ -300,13 +383,13 @@ export class UIService extends Service {
|
|
|
300
383
|
|
|
301
384
|
export default facade(UIService);
|
|
302
385
|
|
|
303
|
-
declare module '
|
|
386
|
+
declare module '@aerogel/core/services/Events' {
|
|
304
387
|
export interface EventsPayload {
|
|
305
388
|
'close-modal': { id: string; result?: unknown };
|
|
306
389
|
'hide-modal': { id: string };
|
|
307
390
|
'hide-overlays-backdrop': void;
|
|
308
|
-
'modal-closed': { modal:
|
|
309
|
-
'modal-will-close': { modal:
|
|
391
|
+
'modal-closed': { modal: UIModal; result?: unknown };
|
|
392
|
+
'modal-will-close': { modal: UIModal; result?: unknown };
|
|
310
393
|
'show-modal': { id: string };
|
|
311
394
|
'show-overlays-backdrop': void;
|
|
312
395
|
}
|
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 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 };
|
|
@@ -24,13 +24,13 @@ 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]:
|
|
32
|
-
[UIComponents.
|
|
33
|
-
[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,
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
Object.entries({
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { computed, unref } from 'vue';
|
|
3
|
+
import { cva } from 'class-variance-authority';
|
|
4
|
+
import { twMerge } from 'tailwind-merge';
|
|
5
|
+
import type { ClassValue } from 'clsx';
|
|
6
|
+
import type { ComputedRef, PropType, Ref } from 'vue';
|
|
7
|
+
import type { GetClosureArgs, GetClosureResult } from '@noeldemartin/utils';
|
|
8
|
+
|
|
9
|
+
export type CVAConfig<T> = NonNullable<GetClosureArgs<typeof cva<T>>[1]>;
|
|
10
|
+
export type CVAProps<T> = NonNullable<GetClosureArgs<GetClosureResult<typeof cva<T>>>[0]>;
|
|
11
|
+
export type RefsObject<T> = { [K in keyof T]: Ref<T[K]> | T[K] };
|
|
12
|
+
export type Variants<T extends Record<string, string | boolean>> = Required<{
|
|
13
|
+
[K in keyof T]: Exclude<T[K], undefined> extends string
|
|
14
|
+
? { [key in Exclude<T[K], undefined>]: string | null }
|
|
15
|
+
: { true: string | null; false: string | null };
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
export type ComponentPropDefinitions<T> = {
|
|
19
|
+
[K in keyof T]: {
|
|
20
|
+
type?: PropType<T[K]>;
|
|
21
|
+
default: T[K] | (() => T[K]) | null;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type PickComponentProps<TValues, TDefinitions> = {
|
|
26
|
+
[K in keyof TValues]: K extends keyof TDefinitions ? TValues[K] : never;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function computedVariantClasses<T>(
|
|
30
|
+
value: RefsObject<{ baseClasses?: string } & CVAProps<T>>,
|
|
31
|
+
config: { baseClasses?: string } & CVAConfig<T>,
|
|
32
|
+
): ComputedRef<string> {
|
|
33
|
+
return computed(() => {
|
|
34
|
+
const { baseClasses: valueBaseClasses, ...valueRefs } = value;
|
|
35
|
+
const { baseClasses: configBaseClasses, ...configs } = config;
|
|
36
|
+
const variants = cva(configBaseClasses, configs as CVAConfig<T>);
|
|
37
|
+
const values = Object.entries(valueRefs).reduce((extractedValues, [name, valueRef]) => {
|
|
38
|
+
extractedValues[name as keyof CVAProps<T>] = unref(valueRef);
|
|
39
|
+
|
|
40
|
+
return extractedValues;
|
|
41
|
+
}, {} as CVAProps<T>);
|
|
42
|
+
|
|
43
|
+
return classes(variants(values), unref(valueBaseClasses));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function classes(...inputs: ClassValue[]): string {
|
|
48
|
+
return twMerge(clsx(inputs));
|
|
49
|
+
}
|
|
@@ -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,21 @@
|
|
|
1
1
|
import { objectWithout } from '@noeldemartin/utils';
|
|
2
|
-
import { computed, useAttrs } from 'vue';
|
|
2
|
+
import { computed, inject, onUnmounted, useAttrs } from 'vue';
|
|
3
|
+
import type { ClassValue } from 'clsx';
|
|
3
4
|
import type { ComputedRef } from 'vue';
|
|
5
|
+
import type { FormController } from '@aerogel/core/forms';
|
|
6
|
+
import type { Nullable } from '@noeldemartin/utils';
|
|
4
7
|
|
|
5
|
-
export function
|
|
8
|
+
export function onFormFocus(input: { name: Nullable<string> }, listener: () => unknown): void {
|
|
9
|
+
const form = inject<FormController | null>('form', null);
|
|
10
|
+
const stop = form?.on('focus', (name) => input.name === name && listener());
|
|
11
|
+
|
|
12
|
+
onUnmounted(() => stop?.());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<ClassValue>] {
|
|
6
16
|
const attrs = useAttrs();
|
|
7
|
-
const
|
|
17
|
+
const classes = computed(() => attrs.class);
|
|
8
18
|
const inputAttrs = computed(() => objectWithout(attrs, 'class'));
|
|
9
19
|
|
|
10
|
-
return [inputAttrs,
|
|
20
|
+
return [inputAttrs, classes as ComputedRef<ClassValue>];
|
|
11
21
|
}
|
|
@@ -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
|
+
});
|