@aerogel/core 0.0.0-next.9a02fcd3bcf698211dd7a71d4c48257c96dd7832 → 0.0.0-next.9a1c5ba39a454b316eba36ec7bdf579fed3d95d2
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 +2130 -1361
- package/dist/aerogel-core.js +2763 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +23 -37
- package/src/bootstrap/bootstrap.test.ts +4 -8
- package/src/bootstrap/index.ts +25 -16
- package/src/bootstrap/options.ts +1 -1
- package/src/components/AGAppLayout.vue +1 -1
- package/src/components/AGAppModals.vue +1 -1
- package/src/components/AGAppOverlays.vue +1 -1
- package/src/components/AGAppSnackbars.vue +1 -1
- package/src/components/composition.ts +23 -0
- package/src/components/contracts/Modal.ts +16 -0
- package/src/components/contracts/index.ts +2 -0
- package/src/components/contracts/shared.ts +9 -0
- package/src/components/forms/AGButton.vue +2 -2
- package/src/components/forms/AGCheckbox.vue +4 -3
- package/src/components/forms/AGForm.vue +11 -12
- package/src/components/forms/AGInput.vue +8 -4
- package/src/components/forms/AGSelect.vue +11 -17
- package/src/components/headless/forms/AGHeadlessButton.ts +3 -0
- package/src/components/headless/forms/AGHeadlessButton.vue +16 -5
- package/src/components/headless/forms/AGHeadlessInput.ts +18 -5
- package/src/components/headless/forms/AGHeadlessInput.vue +19 -6
- package/src/components/headless/forms/AGHeadlessInputDescription.vue +28 -0
- package/src/components/headless/forms/AGHeadlessInputError.vue +2 -2
- package/src/components/headless/forms/AGHeadlessInputInput.vue +43 -6
- package/src/components/headless/forms/AGHeadlessInputLabel.vue +1 -1
- package/src/components/headless/forms/AGHeadlessInputTextArea.vue +43 -0
- package/src/components/headless/forms/AGHeadlessSelect.ts +3 -3
- package/src/components/headless/forms/AGHeadlessSelect.vue +15 -15
- 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 +10 -0
- package/src/components/headless/forms/index.ts +6 -3
- package/src/components/headless/modals/AGHeadlessModal.ts +17 -18
- package/src/components/headless/modals/AGHeadlessModal.vue +12 -10
- package/src/components/headless/modals/AGHeadlessModalContent.vue +25 -0
- package/src/components/headless/modals/index.ts +3 -2
- package/src/components/headless/snackbars/index.ts +3 -3
- package/src/components/index.ts +2 -0
- package/src/components/lib/AGErrorMessage.vue +3 -3
- package/src/components/lib/AGMarkdown.vue +24 -6
- package/src/components/lib/AGMeasured.vue +3 -2
- package/src/components/lib/AGProgressBar.vue +55 -0
- package/src/components/lib/AGStartupCrash.vue +1 -1
- package/src/components/lib/index.ts +1 -0
- package/src/components/modals/AGAlertModal.ts +6 -3
- package/src/components/modals/AGConfirmModal.ts +19 -4
- package/src/components/modals/AGConfirmModal.vue +6 -5
- package/src/components/modals/AGErrorReportModal.ts +8 -5
- package/src/components/modals/AGErrorReportModal.vue +2 -2
- package/src/components/modals/AGErrorReportModalButtons.vue +10 -10
- package/src/components/modals/AGErrorReportModalTitle.vue +2 -2
- package/src/components/modals/AGLoadingModal.ts +11 -5
- package/src/components/modals/AGModal.vue +20 -19
- package/src/components/modals/AGModalContext.ts +1 -1
- package/src/components/modals/AGModalContext.vue +15 -5
- package/src/components/modals/AGModalTitle.vue +1 -1
- package/src/components/modals/AGPromptModal.ts +15 -4
- package/src/components/modals/AGPromptModal.vue +5 -4
- package/src/components/modals/index.ts +0 -1
- package/src/components/snackbars/AGSnackbar.vue +2 -2
- package/src/components/utils.ts +62 -9
- package/src/directives/index.ts +11 -5
- package/src/directives/measure.ts +34 -6
- package/src/errors/Errors.state.ts +1 -1
- package/src/errors/Errors.ts +12 -12
- 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 +32 -3
- package/src/forms/Form.ts +80 -20
- package/src/forms/composition.ts +2 -2
- package/src/forms/index.ts +3 -1
- package/src/forms/utils.ts +34 -3
- package/src/forms/validation.ts +19 -0
- package/src/{main.ts → index.ts} +1 -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 +46 -0
- package/src/lang/Lang.state.ts +11 -0
- package/src/lang/Lang.ts +48 -21
- package/src/lang/index.ts +8 -6
- package/src/plugins/Plugin.ts +1 -1
- package/src/plugins/index.ts +10 -7
- package/src/services/App.state.ts +26 -3
- package/src/services/App.ts +11 -3
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.ts +15 -5
- package/src/services/Service.ts +125 -54
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +13 -5
- package/src/services/utils.ts +18 -0
- package/src/testing/index.ts +4 -3
- package/src/testing/setup.ts +11 -0
- package/src/ui/UI.state.ts +9 -2
- package/src/ui/UI.ts +157 -52
- package/src/ui/index.ts +5 -4
- package/src/ui/utils.ts +16 -0
- package/src/utils/composition/events.ts +2 -2
- 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 +22 -15
- 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/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/AGHeadlessModalPanel.vue +0 -28
- package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
- package/src/components/modals/AGModal.ts +0 -10
- package/src/directives/initial-focus.ts +0 -11
- package/src/main.histoire.ts +0 -1
- package/tailwind.config.js +0 -4
- package/tsconfig.json +0 -11
- package/vite.config.ts +0 -14
package/src/services/index.ts
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import type { App as VueApp } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { definePlugin } from '
|
|
3
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
4
|
+
import { isDevelopment, isTesting } from '@noeldemartin/utils';
|
|
4
5
|
|
|
5
6
|
import App from './App';
|
|
7
|
+
import Cache from './Cache';
|
|
6
8
|
import Events from './Events';
|
|
7
9
|
import Service from './Service';
|
|
10
|
+
import Storage from './Storage';
|
|
8
11
|
import { getPiniaStore } from './store';
|
|
9
12
|
|
|
10
13
|
export * from './App';
|
|
14
|
+
export * from './Cache';
|
|
11
15
|
export * from './Events';
|
|
12
16
|
export * from './Service';
|
|
13
17
|
export * from './store';
|
|
18
|
+
export * from './utils';
|
|
14
19
|
|
|
15
|
-
export { App, Events, Service };
|
|
20
|
+
export { App, Cache, Events, Storage, Service };
|
|
16
21
|
|
|
17
22
|
const defaultServices = {
|
|
18
23
|
$app: App,
|
|
19
24
|
$events: Events,
|
|
25
|
+
$storage: Storage,
|
|
20
26
|
};
|
|
21
27
|
|
|
22
28
|
export type DefaultServices = typeof defaultServices;
|
|
@@ -34,7 +40,9 @@ export async function bootServices(app: VueApp, services: Record<string, Service
|
|
|
34
40
|
|
|
35
41
|
Object.assign(app.config.globalProperties, services);
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
if (isDevelopment() || isTesting()) {
|
|
44
|
+
Object.assign(globalThis, services);
|
|
45
|
+
}
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
export default definePlugin({
|
|
@@ -50,12 +58,12 @@ export default definePlugin({
|
|
|
50
58
|
},
|
|
51
59
|
});
|
|
52
60
|
|
|
53
|
-
declare module '
|
|
61
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
54
62
|
export interface AerogelOptions {
|
|
55
63
|
services?: Record<string, Service>;
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
declare module '
|
|
67
|
+
declare module 'vue' {
|
|
60
68
|
interface ComponentCustomProperties extends Services {}
|
|
61
69
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { objectOnly } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
export type Replace<
|
|
4
|
+
TOriginal extends Record<string, unknown>,
|
|
5
|
+
TReplacements extends Partial<Record<keyof TOriginal, unknown>>,
|
|
6
|
+
> = {
|
|
7
|
+
[K in keyof TOriginal]: TReplacements extends Record<K, infer Replacement> ? Replacement : TOriginal[K];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function replaceExisting<
|
|
11
|
+
TOriginal extends Record<string, unknown>,
|
|
12
|
+
TReplacements extends Partial<Record<keyof TOriginal, unknown>>,
|
|
13
|
+
>(original: TOriginal, replacements: TReplacements): Replace<TOriginal, TReplacements> {
|
|
14
|
+
return {
|
|
15
|
+
...original,
|
|
16
|
+
...objectOnly(replacements, Object.keys(original)),
|
|
17
|
+
} as Replace<TOriginal, TReplacements>;
|
|
18
|
+
}
|
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
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FakeLocalStorage } from '@noeldemartin/testing';
|
|
2
|
+
import { beforeEach, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
vi.mock('dompurify', async () => {
|
|
5
|
+
return { default: { sanitize: (html: string) => html } };
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
FakeLocalStorage.reset();
|
|
10
|
+
FakeLocalStorage.patchGlobal();
|
|
11
|
+
});
|
package/src/ui/UI.state.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Component } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { defineServiceState } from '
|
|
3
|
+
import { defineServiceState } from '@aerogel/core/services/Service';
|
|
4
|
+
|
|
5
|
+
import { Layouts, getCurrentLayout } from './utils';
|
|
4
6
|
|
|
5
7
|
export interface Modal<T = unknown> {
|
|
6
8
|
id: string;
|
|
@@ -14,7 +16,7 @@ export interface ModalComponent<
|
|
|
14
16
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
15
17
|
Properties extends Record<string, unknown> = Record<string, unknown>,
|
|
16
18
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
17
|
-
Result = unknown
|
|
19
|
+
Result = unknown,
|
|
18
20
|
> {}
|
|
19
21
|
|
|
20
22
|
export interface Snackbar {
|
|
@@ -28,5 +30,10 @@ export default defineServiceState({
|
|
|
28
30
|
initialState: {
|
|
29
31
|
modals: [] as Modal[],
|
|
30
32
|
snackbars: [] as Snackbar[],
|
|
33
|
+
layout: getCurrentLayout(),
|
|
34
|
+
},
|
|
35
|
+
computed: {
|
|
36
|
+
mobile: ({ layout }) => layout === Layouts.Mobile,
|
|
37
|
+
desktop: ({ layout }) => layout === Layouts.Desktop,
|
|
31
38
|
},
|
|
32
39
|
});
|
package/src/ui/UI.ts
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
import { after, facade, fail, uuid } from '@noeldemartin/utils';
|
|
1
|
+
import { after, facade, fail, isDevelopment, required, uuid } from '@noeldemartin/utils';
|
|
2
2
|
import { markRaw, nextTick } from 'vue';
|
|
3
3
|
import type { Component } from 'vue';
|
|
4
4
|
import type { ObjectValues } from '@noeldemartin/utils';
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import type {
|
|
6
|
+
import App from '@aerogel/core/services/App';
|
|
7
|
+
import Events from '@aerogel/core/services/Events';
|
|
8
|
+
import type { AcceptRefs } from '@aerogel/core/utils';
|
|
9
|
+
import type { Color } from '@aerogel/core/components/constants';
|
|
10
|
+
import type { SnackbarAction, SnackbarColor } from '@aerogel/core/components/headless/snackbars';
|
|
11
|
+
import type {
|
|
12
|
+
AGAlertModalProps,
|
|
13
|
+
AGConfirmModalProps,
|
|
14
|
+
AGLoadingModalProps,
|
|
15
|
+
AGPromptModalProps,
|
|
16
|
+
} from '@aerogel/core/components';
|
|
9
17
|
|
|
10
18
|
import Service from './UI.state';
|
|
19
|
+
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
11
20
|
import type { Modal, ModalComponent, Snackbar } from './UI.state';
|
|
12
21
|
|
|
13
22
|
interface ModalCallbacks<T = unknown> {
|
|
@@ -16,9 +25,8 @@ interface ModalCallbacks<T = unknown> {
|
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
|
|
19
|
-
type ModalResult<TComponent> =
|
|
20
|
-
? TResult
|
|
21
|
-
: never;
|
|
28
|
+
type ModalResult<TComponent> =
|
|
29
|
+
TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
|
|
22
30
|
|
|
23
31
|
export const UIComponents = {
|
|
24
32
|
AlertModal: 'alert-modal',
|
|
@@ -32,18 +40,37 @@ export const UIComponents = {
|
|
|
32
40
|
|
|
33
41
|
export type UIComponent = ObjectValues<typeof UIComponents>;
|
|
34
42
|
|
|
35
|
-
export
|
|
43
|
+
export type ConfirmCheckboxes = Record<string, { label: string; default?: boolean; required?: boolean }>;
|
|
44
|
+
|
|
45
|
+
export type ConfirmOptions = AcceptRefs<{
|
|
36
46
|
acceptText?: string;
|
|
47
|
+
acceptColor?: Color;
|
|
37
48
|
cancelText?: string;
|
|
49
|
+
cancelColor?: Color;
|
|
50
|
+
actions?: Record<string, () => unknown>;
|
|
51
|
+
required?: boolean;
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
export type LoadingOptions = AcceptRefs<{
|
|
55
|
+
title?: string;
|
|
56
|
+
message?: string;
|
|
57
|
+
progress?: number;
|
|
58
|
+
}>;
|
|
59
|
+
|
|
60
|
+
export interface ConfirmOptionsWithCheckboxes<T extends ConfirmCheckboxes = ConfirmCheckboxes> extends ConfirmOptions {
|
|
61
|
+
checkboxes?: T;
|
|
38
62
|
}
|
|
39
63
|
|
|
40
|
-
export
|
|
64
|
+
export type PromptOptions = AcceptRefs<{
|
|
41
65
|
label?: string;
|
|
42
66
|
defaultValue?: string;
|
|
43
67
|
placeholder?: string;
|
|
44
68
|
acceptText?: string;
|
|
69
|
+
acceptColor?: Color;
|
|
45
70
|
cancelText?: string;
|
|
46
|
-
|
|
71
|
+
cancelColor?: Color;
|
|
72
|
+
trim?: boolean;
|
|
73
|
+
}>;
|
|
47
74
|
|
|
48
75
|
export interface ShowSnackbarOptions {
|
|
49
76
|
component?: Component;
|
|
@@ -77,35 +104,66 @@ export class UIService extends Service {
|
|
|
77
104
|
this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
|
|
78
105
|
}
|
|
79
106
|
|
|
107
|
+
/* eslint-disable max-len */
|
|
80
108
|
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
81
109
|
public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
110
|
+
public async confirm<T extends ConfirmCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
111
|
+
public async confirm<T extends ConfirmCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
112
|
+
/* eslint-enable max-len */
|
|
113
|
+
|
|
82
114
|
public async confirm(
|
|
83
115
|
messageOrTitle: string,
|
|
84
|
-
messageOrOptions?: string | ConfirmOptions,
|
|
85
|
-
options?: ConfirmOptions,
|
|
86
|
-
): Promise<boolean> {
|
|
116
|
+
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
117
|
+
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
118
|
+
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
87
119
|
const getProperties = (): AGConfirmModalProps => {
|
|
88
120
|
if (typeof messageOrOptions !== 'string') {
|
|
89
121
|
return {
|
|
90
|
-
message: messageOrTitle,
|
|
91
122
|
...(messageOrOptions ?? {}),
|
|
123
|
+
message: messageOrTitle,
|
|
124
|
+
required: !!messageOrOptions?.required,
|
|
92
125
|
};
|
|
93
126
|
}
|
|
94
127
|
|
|
95
128
|
return {
|
|
129
|
+
...(options ?? {}),
|
|
96
130
|
title: messageOrTitle,
|
|
97
131
|
message: messageOrOptions,
|
|
98
|
-
|
|
132
|
+
required: !!options?.required,
|
|
99
133
|
};
|
|
100
134
|
};
|
|
101
|
-
|
|
102
|
-
const modal = await this.openModal<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
);
|
|
135
|
+
const properties = getProperties();
|
|
136
|
+
const modal = await this.openModal<
|
|
137
|
+
ModalComponent<AGConfirmModalProps, boolean | [boolean, Record<string, boolean>]>
|
|
138
|
+
>(this.requireComponent(UIComponents.ConfirmModal), properties);
|
|
106
139
|
const result = await modal.beforeClose;
|
|
107
140
|
|
|
108
|
-
|
|
141
|
+
const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
|
|
142
|
+
const checkboxes =
|
|
143
|
+
typeof result === 'object'
|
|
144
|
+
? result[1]
|
|
145
|
+
: Object.entries(properties.checkboxes ?? {}).reduce(
|
|
146
|
+
(values, [checkbox, { default: defaultValue }]) => ({
|
|
147
|
+
[checkbox]: defaultValue ?? false,
|
|
148
|
+
...values,
|
|
149
|
+
}),
|
|
150
|
+
{} as Record<string, boolean>,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
|
|
154
|
+
if (!checkbox.required || checkboxes[name]) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (confirmed && isDevelopment()) {
|
|
159
|
+
// eslint-disable-next-line no-console
|
|
160
|
+
console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return [false, checkboxes];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
|
|
109
167
|
}
|
|
110
168
|
|
|
111
169
|
public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
@@ -115,47 +173,63 @@ export class UIService extends Service {
|
|
|
115
173
|
messageOrOptions?: string | PromptOptions,
|
|
116
174
|
options?: PromptOptions,
|
|
117
175
|
): Promise<string | null> {
|
|
176
|
+
const trim = options?.trim ?? true;
|
|
118
177
|
const getProperties = (): AGPromptModalProps => {
|
|
119
178
|
if (typeof messageOrOptions !== 'string') {
|
|
120
179
|
return {
|
|
121
180
|
message: messageOrTitle,
|
|
122
181
|
...(messageOrOptions ?? {}),
|
|
123
|
-
};
|
|
182
|
+
} as AGPromptModalProps;
|
|
124
183
|
}
|
|
125
184
|
|
|
126
185
|
return {
|
|
127
186
|
title: messageOrTitle,
|
|
128
187
|
message: messageOrOptions,
|
|
129
188
|
...(options ?? {}),
|
|
130
|
-
};
|
|
189
|
+
} as AGPromptModalProps;
|
|
131
190
|
};
|
|
132
191
|
|
|
133
192
|
const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
|
|
134
193
|
this.requireComponent(UIComponents.PromptModal),
|
|
135
194
|
getProperties(),
|
|
136
195
|
);
|
|
137
|
-
const
|
|
196
|
+
const rawResult = await modal.beforeClose;
|
|
197
|
+
const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
|
|
138
198
|
|
|
139
199
|
return result ?? null;
|
|
140
200
|
}
|
|
141
201
|
|
|
142
|
-
public async loading<T>(operation: Promise<T>): Promise<T>;
|
|
143
|
-
public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
|
|
144
|
-
public async loading<T>(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
202
|
+
public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
|
|
203
|
+
public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
|
|
204
|
+
public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
|
|
205
|
+
public async loading<T>(
|
|
206
|
+
operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
|
|
207
|
+
operation?: Promise<T> | (() => T),
|
|
208
|
+
): Promise<T> {
|
|
209
|
+
const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
|
|
210
|
+
const processArgs = (): { operationPromise: Promise<T>; props?: AGLoadingModalProps } => {
|
|
211
|
+
if (typeof operationOrMessageOrOptions === 'string') {
|
|
212
|
+
return {
|
|
213
|
+
props: { message: operationOrMessageOrOptions },
|
|
214
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
215
|
+
};
|
|
148
216
|
}
|
|
149
217
|
|
|
150
|
-
|
|
218
|
+
if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
|
|
219
|
+
return { operationPromise: processOperation(operationOrMessageOrOptions) };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
props: operationOrMessageOrOptions,
|
|
224
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
225
|
+
};
|
|
151
226
|
};
|
|
152
227
|
|
|
153
|
-
const
|
|
228
|
+
const { operationPromise, props } = processArgs();
|
|
229
|
+
const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
|
|
154
230
|
|
|
155
231
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const [result] = await Promise.all([operation, after({ seconds: 1 })]);
|
|
232
|
+
const [result] = await Promise.all([operationPromise, after({ seconds: 1 })]);
|
|
159
233
|
|
|
160
234
|
return result;
|
|
161
235
|
} finally {
|
|
@@ -217,12 +291,40 @@ export class UIService extends Service {
|
|
|
217
291
|
}
|
|
218
292
|
|
|
219
293
|
public async closeModal(id: string, result?: unknown): Promise<void> {
|
|
294
|
+
if (!App.isMounted()) {
|
|
295
|
+
await this.removeModal(id, result);
|
|
296
|
+
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
220
300
|
await Events.emit('close-modal', { id, result });
|
|
221
301
|
}
|
|
222
302
|
|
|
223
|
-
|
|
303
|
+
public async closeAllModals(): Promise<void> {
|
|
304
|
+
while (this.modals.length > 0) {
|
|
305
|
+
await this.closeModal(required(this.modals[this.modals.length - 1]).id);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
protected override async boot(): Promise<void> {
|
|
224
310
|
this.watchModalEvents();
|
|
225
311
|
this.watchMountedEvent();
|
|
312
|
+
this.watchViewportBreakpoints();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private async removeModal(id: string, result?: unknown): Promise<void> {
|
|
316
|
+
this.setState(
|
|
317
|
+
'modals',
|
|
318
|
+
this.modals.filter((m) => m.id !== id),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
this.modalCallbacks[id]?.closed?.(result);
|
|
322
|
+
|
|
323
|
+
delete this.modalCallbacks[id];
|
|
324
|
+
|
|
325
|
+
const activeModal = this.modals.at(-1);
|
|
326
|
+
|
|
327
|
+
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
226
328
|
}
|
|
227
329
|
|
|
228
330
|
private watchModalEvents(): void {
|
|
@@ -234,31 +336,24 @@ export class UIService extends Service {
|
|
|
234
336
|
}
|
|
235
337
|
});
|
|
236
338
|
|
|
237
|
-
Events.on('modal-closed', async ({ modal, result }) => {
|
|
238
|
-
this.
|
|
239
|
-
'modals',
|
|
240
|
-
this.modals.filter((m) => m.id !== modal.id),
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
this.modalCallbacks[modal.id]?.closed?.(result);
|
|
244
|
-
|
|
245
|
-
delete this.modalCallbacks[modal.id];
|
|
246
|
-
|
|
247
|
-
const activeModal = this.modals.at(-1);
|
|
248
|
-
|
|
249
|
-
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
339
|
+
Events.on('modal-closed', async ({ modal: { id }, result }) => {
|
|
340
|
+
await this.removeModal(id, result);
|
|
250
341
|
});
|
|
251
342
|
}
|
|
252
343
|
|
|
253
344
|
private watchMountedEvent(): void {
|
|
254
345
|
Events.once('application-mounted', async () => {
|
|
255
|
-
|
|
346
|
+
if (!globalThis.document || !globalThis.getComputedStyle) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const splash = globalThis.document.getElementById('splash');
|
|
256
351
|
|
|
257
352
|
if (!splash) {
|
|
258
353
|
return;
|
|
259
354
|
}
|
|
260
355
|
|
|
261
|
-
if (
|
|
356
|
+
if (globalThis.getComputedStyle(splash).opacity !== '0') {
|
|
262
357
|
splash.style.opacity = '0';
|
|
263
358
|
|
|
264
359
|
await after({ ms: 600 });
|
|
@@ -268,11 +363,21 @@ export class UIService extends Service {
|
|
|
268
363
|
});
|
|
269
364
|
}
|
|
270
365
|
|
|
366
|
+
private watchViewportBreakpoints(): void {
|
|
367
|
+
if (!globalThis.matchMedia) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
|
|
372
|
+
|
|
373
|
+
media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
|
|
374
|
+
}
|
|
375
|
+
|
|
271
376
|
}
|
|
272
377
|
|
|
273
378
|
export default facade(UIService);
|
|
274
379
|
|
|
275
|
-
declare module '
|
|
380
|
+
declare module '@aerogel/core/services/Events' {
|
|
276
381
|
export interface EventsPayload {
|
|
277
382
|
'close-modal': { id: string; result?: unknown };
|
|
278
383
|
'hide-modal': { id: string };
|
package/src/ui/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Component } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { bootServices } from '
|
|
4
|
-
import { definePlugin } from '
|
|
3
|
+
import { bootServices } from '@aerogel/core/services';
|
|
4
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
5
5
|
|
|
6
6
|
import UI, { UIComponents } from './UI';
|
|
7
7
|
import AGAlertModal from '../components/modals/AGAlertModal.vue';
|
|
@@ -16,6 +16,7 @@ import type { UIComponent } from './UI';
|
|
|
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;
|
|
@@ -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>(
|
|
@@ -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