@aerogel/core 0.0.0-next.f1f5a990033d966dc0bb12d251110fbc9350dcc7 → 0.0.0-next.f8c757d83e1e0d001a2836fa45aba318ec17b9b9
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 +2144 -1268
- package/dist/aerogel-core.js +3234 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +30 -37
- package/src/bootstrap/bootstrap.test.ts +4 -8
- package/src/bootstrap/index.ts +27 -14
- package/src/bootstrap/options.ts +4 -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 +18 -0
- package/src/components/contracts/ErrorReportModal.ts +29 -0
- package/src/components/contracts/Input.ts +26 -0
- package/src/components/contracts/LoadingModal.ts +18 -0
- package/src/components/contracts/Modal.ts +13 -0
- package/src/components/contracts/PromptModal.ts +30 -0
- package/src/components/contracts/Select.ts +44 -0
- package/src/components/contracts/Toast.ts +13 -0
- package/src/components/contracts/index.ts +9 -0
- package/src/components/contracts/shared.ts +9 -0
- package/src/components/headless/HeadlessButton.vue +51 -0
- package/src/components/headless/HeadlessInput.vue +59 -0
- package/src/components/headless/HeadlessInputDescription.vue +27 -0
- package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
- package/src/components/headless/HeadlessInputInput.vue +75 -0
- package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
- package/src/components/headless/HeadlessInputTextArea.vue +40 -0
- package/src/components/headless/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +18 -18
- package/src/components/headless/HeadlessModalContent.vue +24 -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 +113 -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 -9
- package/src/components/ui/AdvancedOptions.vue +18 -0
- package/src/components/ui/AlertModal.vue +13 -0
- package/src/components/ui/Button.vue +98 -0
- package/src/components/ui/Checkbox.vue +56 -0
- package/src/components/ui/ConfirmModal.vue +42 -0
- package/src/components/ui/DropdownMenu.vue +27 -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/ui/Form.vue +24 -0
- package/src/components/ui/Input.vue +56 -0
- package/src/components/ui/Link.vue +12 -0
- package/src/components/ui/LoadingModal.vue +32 -0
- package/src/components/ui/Markdown.vue +69 -0
- package/src/components/ui/Modal.vue +75 -0
- package/src/components/ui/ModalContext.vue +30 -0
- package/src/components/ui/ProgressBar.vue +50 -0
- package/src/components/ui/PromptModal.vue +35 -0
- package/src/components/ui/Select.vue +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 +11 -5
- package/src/directives/measure.ts +34 -6
- package/src/errors/Errors.state.ts +1 -1
- package/src/errors/Errors.ts +29 -33
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +10 -16
- package/src/errors/utils.ts +35 -0
- package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
- package/src/forms/{Form.ts → FormController.ts} +86 -25
- package/src/forms/composition.ts +4 -4
- package/src/forms/index.ts +3 -1
- package/src/forms/utils.ts +36 -5
- package/src/forms/validation.ts +19 -0
- package/src/index.css +46 -0
- package/src/{main.ts → index.ts} +3 -0
- package/src/jobs/Job.ts +147 -0
- package/src/jobs/index.ts +10 -0
- package/src/jobs/listeners.ts +3 -0
- package/src/jobs/status.ts +4 -0
- package/src/lang/DefaultLangProvider.ts +46 -0
- package/src/lang/Lang.state.ts +11 -0
- package/src/lang/Lang.ts +44 -29
- package/src/lang/index.ts +11 -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 +37 -5
- package/src/services/App.ts +35 -6
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.test.ts +39 -0
- package/src/services/Events.ts +112 -32
- package/src/services/Service.ts +150 -49
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +17 -5
- package/src/services/store.ts +8 -5
- package/src/services/utils.ts +18 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/setup.ts +11 -0
- package/src/ui/UI.state.ts +19 -7
- package/src/ui/UI.ts +218 -70
- package/src/ui/index.ts +19 -16
- package/src/ui/utils.ts +16 -0
- 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 +4 -1
- package/src/utils/markdown.test.ts +50 -0
- package/src/utils/markdown.ts +19 -6
- package/src/utils/vue.ts +28 -127
- package/dist/aerogel-core.cjs.js +0 -2
- package/dist/aerogel-core.cjs.js.map +0 -1
- package/dist/aerogel-core.esm.js +0 -2
- package/dist/aerogel-core.esm.js.map +0 -1
- package/histoire.config.ts +0 -7
- package/noeldemartin.config.js +0 -5
- package/postcss.config.js +0 -6
- package/src/assets/histoire.css +0 -3
- package/src/components/AGAppSnackbars.vue +0 -13
- package/src/components/constants.ts +0 -8
- package/src/components/forms/AGButton.vue +0 -44
- package/src/components/forms/AGCheckbox.vue +0 -35
- package/src/components/forms/AGForm.vue +0 -26
- package/src/components/forms/AGInput.vue +0 -36
- package/src/components/forms/AGSelect.story.vue +0 -28
- package/src/components/forms/AGSelect.vue +0 -53
- package/src/components/forms/index.ts +0 -5
- package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
- package/src/components/headless/forms/AGHeadlessInput.ts +0 -28
- package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
- package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
- package/src/components/headless/forms/AGHeadlessSelect.ts +0 -39
- package/src/components/headless/forms/AGHeadlessSelect.vue +0 -76
- package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
- package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
- package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
- package/src/components/headless/forms/index.ts +0 -14
- package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
- package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
- package/src/components/headless/modals/index.ts +0 -4
- package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
- package/src/components/headless/snackbars/index.ts +0 -40
- package/src/components/lib/AGErrorMessage.vue +0 -16
- package/src/components/lib/AGLink.vue +0 -9
- package/src/components/lib/AGMarkdown.vue +0 -36
- package/src/components/lib/AGMeasured.vue +0 -15
- package/src/components/lib/index.ts +0 -5
- package/src/components/modals/AGAlertModal.ts +0 -15
- package/src/components/modals/AGAlertModal.vue +0 -14
- package/src/components/modals/AGConfirmModal.ts +0 -27
- package/src/components/modals/AGConfirmModal.vue +0 -26
- package/src/components/modals/AGErrorReportModal.ts +0 -46
- package/src/components/modals/AGErrorReportModal.vue +0 -54
- package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
- package/src/components/modals/AGLoadingModal.ts +0 -23
- package/src/components/modals/AGLoadingModal.vue +0 -15
- package/src/components/modals/AGModal.ts +0 -10
- package/src/components/modals/AGModal.vue +0 -39
- package/src/components/modals/AGModalContext.ts +0 -8
- package/src/components/modals/AGModalContext.vue +0 -22
- package/src/components/modals/AGModalTitle.vue +0 -9
- package/src/components/modals/index.ts +0 -26
- 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 -14
package/src/services/index.ts
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
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';
|
|
12
|
+
import type { AppSetting } from './App.state';
|
|
9
13
|
|
|
10
14
|
export * from './App';
|
|
15
|
+
export * from './Cache';
|
|
11
16
|
export * from './Events';
|
|
12
17
|
export * from './Service';
|
|
18
|
+
export * from './store';
|
|
19
|
+
export * from './utils';
|
|
13
20
|
|
|
14
|
-
export { App, Events, Service };
|
|
21
|
+
export { App, Cache, Events, Storage, Service };
|
|
15
22
|
|
|
16
23
|
const defaultServices = {
|
|
17
24
|
$app: App,
|
|
18
25
|
$events: Events,
|
|
26
|
+
$storage: Storage,
|
|
19
27
|
};
|
|
20
28
|
|
|
21
29
|
export type DefaultServices = typeof defaultServices;
|
|
@@ -33,7 +41,9 @@ export async function bootServices(app: VueApp, services: Record<string, Service
|
|
|
33
41
|
|
|
34
42
|
Object.assign(app.config.globalProperties, services);
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
if (isDevelopment() || isTesting()) {
|
|
45
|
+
Object.assign(globalThis, services);
|
|
46
|
+
}
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
export default definePlugin({
|
|
@@ -44,17 +54,19 @@ export default definePlugin({
|
|
|
44
54
|
};
|
|
45
55
|
|
|
46
56
|
app.use(getPiniaStore());
|
|
57
|
+
App.settings.push(...(options.settings ?? []));
|
|
47
58
|
|
|
48
59
|
await bootServices(app, services);
|
|
49
60
|
},
|
|
50
61
|
});
|
|
51
62
|
|
|
52
|
-
declare module '
|
|
63
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
53
64
|
export interface AerogelOptions {
|
|
54
65
|
services?: Record<string, Service>;
|
|
66
|
+
settings?: AppSetting[];
|
|
55
67
|
}
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
declare module '
|
|
70
|
+
declare module 'vue' {
|
|
59
71
|
interface ComponentCustomProperties extends Services {}
|
|
60
72
|
}
|
package/src/services/store.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
import { tap } from '@noeldemartin/utils';
|
|
1
2
|
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
|
2
3
|
import type { DefineStoreOptions, Pinia, StateTree, Store, _GettersTree } from 'pinia';
|
|
3
4
|
|
|
4
5
|
let _store: Pinia | null = null;
|
|
5
6
|
|
|
6
7
|
function initializePiniaStore(): Pinia {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
return _store ?? resetPiniaStore();
|
|
9
|
+
}
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
export function resetPiniaStore(): Pinia {
|
|
12
|
+
return tap(createPinia(), (store) => {
|
|
13
|
+
_store = store;
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
setActivePinia(store);
|
|
16
|
+
});
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export function getPiniaStore(): Pinia {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isTesting } from '@noeldemartin/utils';
|
|
2
|
+
import type { GetClosureArgs } from '@noeldemartin/utils';
|
|
3
|
+
|
|
4
|
+
import Events from '@aerogel/core/services/Events';
|
|
5
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
6
|
+
|
|
7
|
+
export interface AerogelTestingRuntime {
|
|
8
|
+
on: (typeof Events)['on'];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default definePlugin({
|
|
12
|
+
async install() {
|
|
13
|
+
if (!isTesting()) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
globalThis.testingRuntime = {
|
|
18
|
+
on: ((...args: GetClosureArgs<(typeof Events)['on']>) => Events.on(...args)) as (typeof Events)['on'],
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
declare global {
|
|
24
|
+
// eslint-disable-next-line no-var
|
|
25
|
+
var testingRuntime: AerogelTestingRuntime | undefined;
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FakeLocalStorage } from '@noeldemartin/testing';
|
|
2
|
+
import { beforeEach, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
vi.mock('dompurify', async () => {
|
|
5
|
+
return { default: { sanitize: (html: string) => html } };
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
FakeLocalStorage.reset();
|
|
10
|
+
FakeLocalStorage.patchGlobal();
|
|
11
|
+
});
|
package/src/ui/UI.state.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Component } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { defineServiceState } from '
|
|
3
|
+
import { defineServiceState } from '@aerogel/core/services/Service';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import { Layouts, getCurrentLayout } from './utils';
|
|
6
|
+
|
|
7
|
+
export interface UIModal<T = unknown> {
|
|
6
8
|
id: string;
|
|
7
9
|
properties: Record<string, unknown>;
|
|
8
10
|
component: Component;
|
|
@@ -10,14 +12,19 @@ export interface Modal<T = unknown> {
|
|
|
10
12
|
afterClose: Promise<T | undefined>;
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
export interface UIModalContext {
|
|
16
|
+
modal: UIModal;
|
|
17
|
+
childIndex?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
export interface ModalComponent<
|
|
14
21
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
15
|
-
Properties extends
|
|
22
|
+
Properties extends object = object,
|
|
16
23
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
17
|
-
Result = unknown
|
|
24
|
+
Result = unknown,
|
|
18
25
|
> {}
|
|
19
26
|
|
|
20
|
-
export interface
|
|
27
|
+
export interface UIToast {
|
|
21
28
|
id: string;
|
|
22
29
|
component: Component;
|
|
23
30
|
properties: Record<string, unknown>;
|
|
@@ -26,7 +33,12 @@ export interface Snackbar {
|
|
|
26
33
|
export default defineServiceState({
|
|
27
34
|
name: 'ui',
|
|
28
35
|
initialState: {
|
|
29
|
-
modals: [] as
|
|
30
|
-
|
|
36
|
+
modals: [] as UIModal[],
|
|
37
|
+
toasts: [] as UIToast[],
|
|
38
|
+
layout: getCurrentLayout(),
|
|
39
|
+
},
|
|
40
|
+
computed: {
|
|
41
|
+
mobile: ({ layout }) => layout === Layouts.Mobile,
|
|
42
|
+
desktop: ({ layout }) => layout === Layouts.Desktop,
|
|
31
43
|
},
|
|
32
44
|
});
|
package/src/ui/UI.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import { after, facade, fail, uuid } from '@noeldemartin/utils';
|
|
1
|
+
import { after, facade, fail, isDevelopment, required, uuid } from '@noeldemartin/utils';
|
|
2
2
|
import { markRaw, nextTick } from 'vue';
|
|
3
3
|
import type { Component } from 'vue';
|
|
4
4
|
import type { ObjectValues } from '@noeldemartin/utils';
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import
|
|
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';
|
|
8
15
|
|
|
9
16
|
import Service from './UI.state';
|
|
10
|
-
import
|
|
11
|
-
import type {
|
|
17
|
+
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
18
|
+
import type { ModalComponent, UIModal, UIToast } from './UI.state';
|
|
12
19
|
|
|
13
20
|
interface ModalCallbacks<T = unknown> {
|
|
14
21
|
willClose(result: T | undefined): void;
|
|
@@ -16,30 +23,56 @@ interface ModalCallbacks<T = unknown> {
|
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
|
|
19
|
-
type ModalResult<TComponent> =
|
|
20
|
-
? TResult
|
|
21
|
-
: never;
|
|
26
|
+
type ModalResult<TComponent> =
|
|
27
|
+
TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
|
|
22
28
|
|
|
23
29
|
export const UIComponents = {
|
|
24
30
|
AlertModal: 'alert-modal',
|
|
25
31
|
ConfirmModal: 'confirm-modal',
|
|
26
32
|
ErrorReportModal: 'error-report-modal',
|
|
27
33
|
LoadingModal: 'loading-modal',
|
|
28
|
-
|
|
34
|
+
PromptModal: 'prompt-modal',
|
|
35
|
+
Toast: 'toast',
|
|
29
36
|
StartupCrash: 'startup-crash',
|
|
30
37
|
} as const;
|
|
31
38
|
|
|
32
39
|
export type UIComponent = ObjectValues<typeof UIComponents>;
|
|
33
40
|
|
|
34
|
-
export
|
|
41
|
+
export type ConfirmOptions = AcceptRefs<{
|
|
35
42
|
acceptText?: string;
|
|
43
|
+
acceptVariant?: ButtonVariant;
|
|
36
44
|
cancelText?: string;
|
|
45
|
+
cancelVariant?: ButtonVariant;
|
|
46
|
+
actions?: Record<string, () => unknown>;
|
|
47
|
+
required?: boolean;
|
|
48
|
+
}>;
|
|
49
|
+
|
|
50
|
+
export type LoadingOptions = AcceptRefs<{
|
|
51
|
+
title?: string;
|
|
52
|
+
message?: string;
|
|
53
|
+
progress?: number;
|
|
54
|
+
}>;
|
|
55
|
+
|
|
56
|
+
export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
|
|
57
|
+
extends ConfirmOptions {
|
|
58
|
+
checkboxes?: T;
|
|
37
59
|
}
|
|
38
60
|
|
|
39
|
-
export
|
|
61
|
+
export type PromptOptions = AcceptRefs<{
|
|
62
|
+
label?: string;
|
|
63
|
+
defaultValue?: string;
|
|
64
|
+
placeholder?: string;
|
|
65
|
+
acceptText?: string;
|
|
66
|
+
acceptVariant?: ButtonVariant;
|
|
67
|
+
cancelText?: string;
|
|
68
|
+
cancelVariant?: ButtonVariant;
|
|
69
|
+
trim?: boolean;
|
|
70
|
+
}>;
|
|
71
|
+
|
|
72
|
+
export interface ToastOptions {
|
|
40
73
|
component?: Component;
|
|
41
|
-
|
|
42
|
-
actions?:
|
|
74
|
+
variant?: ToastVariant;
|
|
75
|
+
actions?: ToastAction[];
|
|
43
76
|
}
|
|
44
77
|
|
|
45
78
|
export class UIService extends Service {
|
|
@@ -54,7 +87,7 @@ export class UIService extends Service {
|
|
|
54
87
|
public alert(message: string): void;
|
|
55
88
|
public alert(title: string, message: string): void;
|
|
56
89
|
public alert(messageOrTitle: string, message?: string): void {
|
|
57
|
-
const getProperties = ():
|
|
90
|
+
const getProperties = (): AlertModalProps => {
|
|
58
91
|
if (typeof message !== 'string') {
|
|
59
92
|
return { message: messageOrTitle };
|
|
60
93
|
}
|
|
@@ -65,57 +98,147 @@ export class UIService extends Service {
|
|
|
65
98
|
};
|
|
66
99
|
};
|
|
67
100
|
|
|
68
|
-
this.openModal(
|
|
101
|
+
this.openModal<ModalComponent<AlertModalProps>>(
|
|
102
|
+
this.requireComponent(UIComponents.AlertModal),
|
|
103
|
+
getProperties(),
|
|
104
|
+
);
|
|
69
105
|
}
|
|
70
106
|
|
|
107
|
+
/* eslint-disable max-len */
|
|
71
108
|
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
72
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
|
+
|
|
73
114
|
public async confirm(
|
|
74
115
|
messageOrTitle: string,
|
|
75
|
-
messageOrOptions?: string | ConfirmOptions,
|
|
76
|
-
options?: ConfirmOptions,
|
|
77
|
-
): Promise<boolean> {
|
|
78
|
-
const getProperties = ():
|
|
116
|
+
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
117
|
+
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
118
|
+
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
119
|
+
const getProperties = (): AcceptRefs<ConfirmModalProps> => {
|
|
79
120
|
if (typeof messageOrOptions !== 'string') {
|
|
80
121
|
return {
|
|
81
|
-
message: messageOrTitle,
|
|
82
122
|
...(messageOrOptions ?? {}),
|
|
123
|
+
message: messageOrTitle,
|
|
124
|
+
required: !!messageOrOptions?.required,
|
|
83
125
|
};
|
|
84
126
|
}
|
|
85
127
|
|
|
86
128
|
return {
|
|
129
|
+
...(options ?? {}),
|
|
87
130
|
title: messageOrTitle,
|
|
88
131
|
message: messageOrOptions,
|
|
89
|
-
|
|
132
|
+
required: !!options?.required,
|
|
90
133
|
};
|
|
91
134
|
};
|
|
92
135
|
|
|
93
|
-
|
|
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>(
|
|
94
143
|
this.requireComponent(UIComponents.ConfirmModal),
|
|
95
|
-
|
|
144
|
+
properties,
|
|
96
145
|
);
|
|
97
146
|
const result = await modal.beforeClose;
|
|
98
147
|
|
|
99
|
-
|
|
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;
|
|
100
174
|
}
|
|
101
175
|
|
|
102
|
-
public async
|
|
103
|
-
public async
|
|
104
|
-
public async
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
176
|
+
public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
177
|
+
public async prompt(title: string, message: string, options?: PromptOptions): Promise<string | null>;
|
|
178
|
+
public async prompt(
|
|
179
|
+
messageOrTitle: string,
|
|
180
|
+
messageOrOptions?: string | PromptOptions,
|
|
181
|
+
options?: PromptOptions,
|
|
182
|
+
): Promise<string | null> {
|
|
183
|
+
const trim = options?.trim ?? true;
|
|
184
|
+
const getProperties = (): PromptModalProps => {
|
|
185
|
+
if (typeof messageOrOptions !== 'string') {
|
|
186
|
+
return {
|
|
187
|
+
message: messageOrTitle,
|
|
188
|
+
...(messageOrOptions ?? {}),
|
|
189
|
+
} as PromptModalProps;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
title: messageOrTitle,
|
|
194
|
+
message: messageOrOptions,
|
|
195
|
+
...(options ?? {}),
|
|
196
|
+
} as PromptModalProps;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
|
|
200
|
+
this.requireComponent(UIComponents.PromptModal),
|
|
201
|
+
getProperties(),
|
|
202
|
+
);
|
|
203
|
+
const rawResult = await modal.beforeClose;
|
|
204
|
+
const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
|
|
205
|
+
|
|
206
|
+
return result ?? null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
|
|
210
|
+
public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
|
|
211
|
+
public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
|
|
212
|
+
public async loading<T>(
|
|
213
|
+
operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
|
|
214
|
+
operation?: Promise<T> | (() => T),
|
|
215
|
+
): Promise<T> {
|
|
216
|
+
const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
|
|
217
|
+
const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
|
|
218
|
+
if (typeof operationOrMessageOrOptions === 'string') {
|
|
219
|
+
return {
|
|
220
|
+
props: { message: operationOrMessageOrOptions },
|
|
221
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
|
|
226
|
+
return { operationPromise: processOperation(operationOrMessageOrOptions) };
|
|
108
227
|
}
|
|
109
228
|
|
|
110
|
-
return {
|
|
229
|
+
return {
|
|
230
|
+
props: operationOrMessageOrOptions,
|
|
231
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
232
|
+
};
|
|
111
233
|
};
|
|
112
234
|
|
|
113
|
-
const
|
|
235
|
+
const { operationPromise, props } = processArgs();
|
|
236
|
+
const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
|
|
114
237
|
|
|
115
238
|
try {
|
|
116
|
-
|
|
239
|
+
const result = await operationPromise;
|
|
117
240
|
|
|
118
|
-
|
|
241
|
+
await after({ ms: 500 });
|
|
119
242
|
|
|
120
243
|
return result;
|
|
121
244
|
} finally {
|
|
@@ -123,23 +246,15 @@ export class UIService extends Service {
|
|
|
123
246
|
}
|
|
124
247
|
}
|
|
125
248
|
|
|
126
|
-
public
|
|
127
|
-
const
|
|
249
|
+
public toast(message: string, options: ToastOptions = {}): void {
|
|
250
|
+
const { component, ...otherOptions } = options;
|
|
251
|
+
const toast: UIToast = {
|
|
128
252
|
id: uuid(),
|
|
129
|
-
properties: { message, ...
|
|
130
|
-
component:
|
|
253
|
+
properties: { message, ...otherOptions },
|
|
254
|
+
component: markRaw(component ?? this.requireComponent(UIComponents.Toast)),
|
|
131
255
|
};
|
|
132
256
|
|
|
133
|
-
this.setState('
|
|
134
|
-
|
|
135
|
-
setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
public hideSnackbar(id: string): void {
|
|
139
|
-
this.setState(
|
|
140
|
-
'snackbars',
|
|
141
|
-
this.snackbars.filter((snackbar) => snackbar.id !== id),
|
|
142
|
-
);
|
|
257
|
+
this.setState('toasts', this.toasts.concat(toast));
|
|
143
258
|
}
|
|
144
259
|
|
|
145
260
|
public registerComponent(name: UIComponent, component: Component): void {
|
|
@@ -149,10 +264,10 @@ export class UIService extends Service {
|
|
|
149
264
|
public async openModal<TModalComponent extends ModalComponent>(
|
|
150
265
|
component: TModalComponent,
|
|
151
266
|
properties?: ModalProperties<TModalComponent>,
|
|
152
|
-
): Promise<
|
|
267
|
+
): Promise<UIModal<ModalResult<TModalComponent>>> {
|
|
153
268
|
const id = uuid();
|
|
154
269
|
const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
|
|
155
|
-
const modal:
|
|
270
|
+
const modal: UIModal<ModalResult<TModalComponent>> = {
|
|
156
271
|
id,
|
|
157
272
|
properties: properties ?? {},
|
|
158
273
|
component: markRaw(component),
|
|
@@ -177,12 +292,40 @@ export class UIService extends Service {
|
|
|
177
292
|
}
|
|
178
293
|
|
|
179
294
|
public async closeModal(id: string, result?: unknown): Promise<void> {
|
|
295
|
+
if (!App.isMounted()) {
|
|
296
|
+
await this.removeModal(id, result);
|
|
297
|
+
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
180
301
|
await Events.emit('close-modal', { id, result });
|
|
181
302
|
}
|
|
182
303
|
|
|
183
|
-
|
|
304
|
+
public async closeAllModals(): Promise<void> {
|
|
305
|
+
while (this.modals.length > 0) {
|
|
306
|
+
await this.closeModal(required(this.modals[this.modals.length - 1]).id);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
protected override async boot(): Promise<void> {
|
|
184
311
|
this.watchModalEvents();
|
|
185
312
|
this.watchMountedEvent();
|
|
313
|
+
this.watchViewportBreakpoints();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private async removeModal(id: string, result?: unknown): Promise<void> {
|
|
317
|
+
this.setState(
|
|
318
|
+
'modals',
|
|
319
|
+
this.modals.filter((m) => m.id !== id),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
this.modalCallbacks[id]?.closed?.(result);
|
|
323
|
+
|
|
324
|
+
delete this.modalCallbacks[id];
|
|
325
|
+
|
|
326
|
+
const activeModal = this.modals.at(-1);
|
|
327
|
+
|
|
328
|
+
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
186
329
|
}
|
|
187
330
|
|
|
188
331
|
private watchModalEvents(): void {
|
|
@@ -194,31 +337,24 @@ export class UIService extends Service {
|
|
|
194
337
|
}
|
|
195
338
|
});
|
|
196
339
|
|
|
197
|
-
Events.on('modal-closed', async ({ modal, result }) => {
|
|
198
|
-
this.
|
|
199
|
-
'modals',
|
|
200
|
-
this.modals.filter((m) => m.id !== modal.id),
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
this.modalCallbacks[modal.id]?.closed?.(result);
|
|
204
|
-
|
|
205
|
-
delete this.modalCallbacks[modal.id];
|
|
206
|
-
|
|
207
|
-
const activeModal = this.modals.at(-1);
|
|
208
|
-
|
|
209
|
-
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
340
|
+
Events.on('modal-closed', async ({ modal: { id }, result }) => {
|
|
341
|
+
await this.removeModal(id, result);
|
|
210
342
|
});
|
|
211
343
|
}
|
|
212
344
|
|
|
213
345
|
private watchMountedEvent(): void {
|
|
214
346
|
Events.once('application-mounted', async () => {
|
|
215
|
-
|
|
347
|
+
if (!globalThis.document || !globalThis.getComputedStyle) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const splash = globalThis.document.getElementById('splash');
|
|
216
352
|
|
|
217
353
|
if (!splash) {
|
|
218
354
|
return;
|
|
219
355
|
}
|
|
220
356
|
|
|
221
|
-
if (
|
|
357
|
+
if (globalThis.getComputedStyle(splash).opacity !== '0') {
|
|
222
358
|
splash.style.opacity = '0';
|
|
223
359
|
|
|
224
360
|
await after({ ms: 600 });
|
|
@@ -228,16 +364,28 @@ export class UIService extends Service {
|
|
|
228
364
|
});
|
|
229
365
|
}
|
|
230
366
|
|
|
367
|
+
private watchViewportBreakpoints(): void {
|
|
368
|
+
if (!globalThis.matchMedia) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
|
|
373
|
+
|
|
374
|
+
media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
|
|
375
|
+
}
|
|
376
|
+
|
|
231
377
|
}
|
|
232
378
|
|
|
233
|
-
export default facade(
|
|
379
|
+
export default facade(UIService);
|
|
234
380
|
|
|
235
|
-
declare module '
|
|
381
|
+
declare module '@aerogel/core/services/Events' {
|
|
236
382
|
export interface EventsPayload {
|
|
237
|
-
'modal-will-close': { modal: Modal; result?: unknown };
|
|
238
|
-
'modal-closed': { modal: Modal; result?: unknown };
|
|
239
383
|
'close-modal': { id: string; result?: unknown };
|
|
240
384
|
'hide-modal': { id: string };
|
|
385
|
+
'hide-overlays-backdrop': void;
|
|
386
|
+
'modal-closed': { modal: UIModal; result?: unknown };
|
|
387
|
+
'modal-will-close': { modal: UIModal; result?: unknown };
|
|
241
388
|
'show-modal': { id: string };
|
|
389
|
+
'show-overlays-backdrop': void;
|
|
242
390
|
}
|
|
243
391
|
}
|