@aerogel/core 0.0.0-next.9a1c5ba39a454b316eba36ec7bdf579fed3d95d2 → 0.0.0-next.9d1e54cc195274e9dd7d57a73fcb8a9a51927dcb
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.css +1 -0
- package/dist/aerogel-core.d.ts +1309 -1587
- package/dist/aerogel-core.js +2637 -1991
- package/dist/aerogel-core.js.map +1 -1
- package/package.json +10 -2
- package/src/components/AppLayout.vue +14 -0
- package/src/components/{AGAppModals.vue → AppModals.vue} +2 -3
- package/src/components/AppOverlays.vue +9 -0
- package/src/components/AppToasts.vue +16 -0
- package/src/components/contracts/AlertModal.ts +19 -0
- package/src/components/contracts/Button.ts +16 -0
- package/src/components/contracts/ConfirmModal.ts +48 -0
- package/src/components/contracts/DropdownMenu.ts +25 -0
- package/src/components/contracts/ErrorReportModal.ts +33 -0
- package/src/components/contracts/Input.ts +26 -0
- package/src/components/contracts/LoadingModal.ts +26 -0
- package/src/components/contracts/Modal.ts +15 -10
- package/src/components/contracts/PromptModal.ts +34 -0
- package/src/components/contracts/Select.ts +45 -0
- package/src/components/contracts/Toast.ts +15 -0
- package/src/components/contracts/index.ts +10 -1
- 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} +6 -7
- package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +2 -6
- package/src/components/headless/{forms/AGHeadlessInputInput.vue → HeadlessInputInput.vue} +16 -25
- package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +2 -6
- package/src/components/headless/{forms/AGHeadlessInputTextArea.vue → HeadlessInputTextArea.vue} +9 -12
- package/src/components/headless/HeadlessModal.vue +57 -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 +120 -0
- package/src/components/headless/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +3 -4
- package/src/components/headless/HeadlessSelectLabel.vue +25 -0
- package/src/components/headless/HeadlessSelectOption.vue +34 -0
- package/src/components/headless/HeadlessSelectOptions.vue +42 -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 +5 -10
- package/src/components/ui/AdvancedOptions.vue +18 -0
- package/src/components/ui/AlertModal.vue +17 -0
- package/src/components/ui/Button.vue +100 -0
- package/src/components/ui/Checkbox.vue +56 -0
- package/src/components/ui/ConfirmModal.vue +50 -0
- package/src/components/ui/DropdownMenu.vue +32 -0
- package/src/components/ui/DropdownMenuOption.vue +22 -0
- package/src/components/ui/DropdownMenuOptions.vue +44 -0
- package/src/components/ui/EditableContent.vue +82 -0
- package/src/components/ui/ErrorLogs.vue +19 -0
- package/src/components/ui/ErrorLogsModal.vue +48 -0
- package/src/components/ui/ErrorMessage.vue +15 -0
- package/src/components/ui/ErrorReportModal.vue +73 -0
- package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +28 -21
- 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 +34 -0
- package/src/components/ui/Markdown.vue +85 -0
- package/src/components/ui/Modal.vue +123 -0
- package/src/components/{modals/AGModalContext.vue → ui/ModalContext.vue} +8 -9
- package/src/components/ui/ProgressBar.vue +51 -0
- package/src/components/ui/PromptModal.vue +38 -0
- package/src/components/ui/Select.vue +27 -0
- package/src/components/ui/SelectLabel.vue +17 -0
- package/src/components/ui/SelectOption.vue +29 -0
- package/src/components/ui/SelectOptions.vue +35 -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} +7 -7
- package/src/components/ui/Toast.vue +46 -0
- package/src/components/ui/index.ts +32 -0
- package/src/directives/measure.ts +11 -5
- package/src/errors/Errors.ts +22 -20
- package/src/forms/{Form.test.ts → FormController.test.ts} +32 -9
- package/src/forms/{Form.ts → FormController.ts} +28 -27
- package/src/forms/index.ts +2 -3
- package/src/forms/utils.ts +35 -35
- package/src/index.css +73 -0
- package/src/lang/index.ts +4 -0
- package/src/lang/settings/Language.vue +48 -0
- package/src/lang/settings/index.ts +10 -0
- package/src/services/App.state.ts +11 -1
- package/src/services/App.ts +9 -1
- package/src/services/Events.test.ts +8 -8
- package/src/services/Events.ts +2 -8
- package/src/services/index.ts +3 -0
- package/src/ui/UI.state.ts +7 -12
- package/src/ui/UI.ts +115 -106
- package/src/ui/index.ts +23 -24
- package/src/utils/classes.ts +41 -0
- package/src/utils/composition/events.ts +2 -4
- package/src/utils/composition/forms.ts +20 -4
- package/src/utils/composition/state.ts +11 -2
- package/src/utils/index.ts +3 -1
- package/src/utils/types.ts +3 -0
- package/src/utils/vue.ts +28 -129
- package/src/components/AGAppLayout.vue +0 -16
- package/src/components/AGAppOverlays.vue +0 -41
- package/src/components/AGAppSnackbars.vue +0 -13
- package/src/components/composition.ts +0 -23
- package/src/components/constants.ts +0 -8
- package/src/components/contracts/shared.ts +0 -9
- package/src/components/forms/AGButton.vue +0 -44
- package/src/components/forms/AGCheckbox.vue +0 -42
- package/src/components/forms/AGInput.vue +0 -42
- package/src/components/forms/AGSelect.story.vue +0 -46
- package/src/components/forms/AGSelect.vue +0 -54
- 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 -41
- 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/AGHeadlessSelectOption.ts +0 -4
- package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -31
- package/src/components/headless/forms/AGHeadlessSelectOptions.vue +0 -19
- package/src/components/headless/forms/AGHeadlessSelectTrigger.vue +0 -25
- package/src/components/headless/forms/composition.ts +0 -10
- package/src/components/headless/forms/index.ts +0 -17
- package/src/components/headless/modals/AGHeadlessModal.ts +0 -33
- package/src/components/headless/modals/AGHeadlessModal.vue +0 -88
- package/src/components/headless/modals/AGHeadlessModalContent.vue +0 -25
- package/src/components/headless/modals/index.ts +0 -5
- 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 -54
- package/src/components/lib/AGMeasured.vue +0 -16
- package/src/components/lib/AGProgressBar.vue +0 -55
- package/src/components/lib/index.ts +0 -6
- package/src/components/modals/AGAlertModal.ts +0 -18
- package/src/components/modals/AGAlertModal.vue +0 -14
- package/src/components/modals/AGConfirmModal.ts +0 -42
- package/src/components/modals/AGConfirmModal.vue +0 -27
- package/src/components/modals/AGErrorReportModal.ts +0 -49
- package/src/components/modals/AGErrorReportModal.vue +0 -54
- package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
- package/src/components/modals/AGLoadingModal.ts +0 -29
- package/src/components/modals/AGLoadingModal.vue +0 -15
- package/src/components/modals/AGModal.vue +0 -40
- package/src/components/modals/AGModalContext.ts +0 -8
- package/src/components/modals/AGModalTitle.vue +0 -9
- package/src/components/modals/AGPromptModal.ts +0 -41
- package/src/components/modals/AGPromptModal.vue +0 -35
- package/src/components/modals/index.ts +0 -16
- package/src/components/snackbars/AGSnackbar.vue +0 -36
- package/src/components/snackbars/index.ts +0 -3
- package/src/components/utils.ts +0 -63
- package/src/forms/composition.ts +0 -6
- package/src/utils/tailwindcss.test.ts +0 -26
- package/src/utils/tailwindcss.ts +0 -7
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Select
|
|
3
|
+
v-model="$lang.locale"
|
|
4
|
+
class="flex flex-col items-start md:flex-row"
|
|
5
|
+
as="div"
|
|
6
|
+
:options
|
|
7
|
+
:render-option="renderLocale"
|
|
8
|
+
>
|
|
9
|
+
<div class="grow">
|
|
10
|
+
<SelectLabel>
|
|
11
|
+
{{ $td('settings.locale', 'Language') }}
|
|
12
|
+
</SelectLabel>
|
|
13
|
+
<Markdown
|
|
14
|
+
lang-key="settings.localeDescription"
|
|
15
|
+
lang-default="Choose the application's language."
|
|
16
|
+
class="mt-1 text-sm text-gray-500"
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
<Button variant="ghost" :as="SelectTrigger" class="grid w-auto outline-none" />
|
|
20
|
+
<SelectOptions />
|
|
21
|
+
</Select>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import Aerogel from 'virtual:aerogel';
|
|
26
|
+
|
|
27
|
+
import { computed } from 'vue';
|
|
28
|
+
|
|
29
|
+
import Markdown from '@aerogel/core/components/ui/Markdown.vue';
|
|
30
|
+
import Button from '@aerogel/core/components/ui/Button.vue';
|
|
31
|
+
import Select from '@aerogel/core/components/ui/Select.vue';
|
|
32
|
+
import SelectLabel from '@aerogel/core/components/ui/SelectLabel.vue';
|
|
33
|
+
import SelectTrigger from '@aerogel/core/components/ui/SelectTrigger.vue';
|
|
34
|
+
import SelectOptions from '@aerogel/core/components/ui/SelectOptions.vue';
|
|
35
|
+
import { Lang, translateWithDefault } from '@aerogel/core/lang';
|
|
36
|
+
|
|
37
|
+
const browserLocale = Lang.getBrowserLocale();
|
|
38
|
+
const options = computed(() => [null, ...Lang.locales]);
|
|
39
|
+
|
|
40
|
+
function renderLocale(locale: string | null): string {
|
|
41
|
+
return (
|
|
42
|
+
(locale && Aerogel.locales[locale]) ??
|
|
43
|
+
translateWithDefault('settings.localeDefault', '{locale} (default)', {
|
|
44
|
+
locale: Aerogel.locales[browserLocale] ?? browserLocale,
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import Aerogel from 'virtual:aerogel';
|
|
2
2
|
|
|
3
3
|
import { getEnv } from '@noeldemartin/utils';
|
|
4
|
-
import type { App } from 'vue';
|
|
4
|
+
import type { App, Component } from 'vue';
|
|
5
5
|
|
|
6
6
|
import { defineServiceState } from '@aerogel/core/services/Service';
|
|
7
7
|
import type { Plugin } from '@aerogel/core/plugins/Plugin';
|
|
8
8
|
|
|
9
|
+
export interface AppSetting {
|
|
10
|
+
component: Component;
|
|
11
|
+
priority: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function defineSettings<T extends AppSetting[]>(settings: T): T {
|
|
15
|
+
return settings;
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
export default defineServiceState({
|
|
10
19
|
name: 'app',
|
|
11
20
|
initialState: {
|
|
@@ -14,6 +23,7 @@ export default defineServiceState({
|
|
|
14
23
|
environment: getEnv() ?? 'development',
|
|
15
24
|
version: Aerogel.version,
|
|
16
25
|
sourceUrl: Aerogel.sourceUrl,
|
|
26
|
+
settings: [] as AppSetting[],
|
|
17
27
|
},
|
|
18
28
|
computed: {
|
|
19
29
|
development: (state) => state.environment === 'development',
|
package/src/services/App.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import Aerogel from 'virtual:aerogel';
|
|
2
2
|
|
|
3
3
|
import { PromisedValue, facade, forever, updateLocationQueryParameters } from '@noeldemartin/utils';
|
|
4
|
+
import { markRaw } from 'vue';
|
|
4
5
|
|
|
5
6
|
import Events from '@aerogel/core/services/Events';
|
|
6
7
|
import type { Plugin } from '@aerogel/core/plugins';
|
|
7
|
-
import type { Services } from '@aerogel/core/services';
|
|
8
|
+
import type { AppSetting, Services } from '@aerogel/core/services';
|
|
8
9
|
|
|
9
10
|
import Service from './App.state';
|
|
10
11
|
|
|
12
|
+
export { defineSettings } from './App.state';
|
|
13
|
+
export type { AppSetting } from './App.state';
|
|
14
|
+
|
|
11
15
|
export class AppService extends Service {
|
|
12
16
|
|
|
13
17
|
public readonly name = Aerogel.name;
|
|
@@ -22,6 +26,10 @@ export class AppService extends Service {
|
|
|
22
26
|
return this.mounted.isResolved();
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
public addSetting(setting: AppSetting): void {
|
|
30
|
+
this.settings.push(markRaw(setting));
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
public async whenReady<T>(callback: () => T): Promise<T> {
|
|
26
34
|
const result = await this.ready.then(callback);
|
|
27
35
|
|
|
@@ -10,12 +10,12 @@ describe('Events', () => {
|
|
|
10
10
|
// Arrange
|
|
11
11
|
let counter = 0;
|
|
12
12
|
|
|
13
|
-
Events.on('
|
|
13
|
+
Events.on('application-mounted', () => counter++);
|
|
14
14
|
|
|
15
15
|
// Act
|
|
16
|
-
await Events.emit('
|
|
17
|
-
await Events.emit('
|
|
18
|
-
await Events.emit('
|
|
16
|
+
await Events.emit('application-mounted');
|
|
17
|
+
await Events.emit('application-mounted');
|
|
18
|
+
await Events.emit('application-mounted');
|
|
19
19
|
|
|
20
20
|
// Assert
|
|
21
21
|
expect(counter).toEqual(3);
|
|
@@ -25,12 +25,12 @@ describe('Events', () => {
|
|
|
25
25
|
// Arrange
|
|
26
26
|
const storage: string[] = [];
|
|
27
27
|
|
|
28
|
-
Events.on('
|
|
29
|
-
Events.on('
|
|
30
|
-
Events.on('
|
|
28
|
+
Events.on('application-mounted', () => storage.push('second'));
|
|
29
|
+
Events.on('application-mounted', { priority: EventListenerPriorities.Low }, () => storage.push('third'));
|
|
30
|
+
Events.on('application-mounted', { priority: EventListenerPriorities.High }, () => storage.push('first'));
|
|
31
31
|
|
|
32
32
|
// Act
|
|
33
|
-
await Events.emit('
|
|
33
|
+
await Events.emit('application-mounted');
|
|
34
34
|
|
|
35
35
|
// Assert
|
|
36
36
|
expect(storage).toEqual(['first', 'second', 'third']);
|
package/src/services/Events.ts
CHANGED
|
@@ -10,7 +10,6 @@ export type AerogelGlobalEvents = Partial<{ [Event in EventWithoutPayload]: () =
|
|
|
10
10
|
Partial<{ [Event in EventWithPayload]: EventListener<EventsPayload[Event]> }>;
|
|
11
11
|
|
|
12
12
|
export type EventListener<T = unknown> = (payload: T) => unknown;
|
|
13
|
-
export type UnknownEvent<T> = T extends keyof EventsPayload ? never : T;
|
|
14
13
|
|
|
15
14
|
export type EventWithoutPayload = {
|
|
16
15
|
[K in keyof EventsPayload]: EventsPayload[K] extends void ? K : never;
|
|
@@ -34,12 +33,12 @@ export class EventsService extends Service {
|
|
|
34
33
|
|
|
35
34
|
protected override async boot(): Promise<void> {
|
|
36
35
|
Object.entries(globalThis.__aerogelEvents__ ?? {}).forEach(([event, listener]) =>
|
|
37
|
-
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
this.on(event as any, listener as EventListener));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
public emit<Event extends EventWithoutPayload>(event: Event): Promise<void>;
|
|
41
41
|
public emit<Event extends EventWithPayload>(event: Event, payload: EventsPayload[Event]): Promise<void>;
|
|
42
|
-
public emit<Event extends string>(event: UnknownEvent<Event>, payload?: unknown): Promise<void>;
|
|
43
42
|
public async emit(event: string, payload?: unknown): Promise<void> {
|
|
44
43
|
const listeners = this.listeners[event] ?? { priorities: [], handlers: {} };
|
|
45
44
|
|
|
@@ -55,9 +54,6 @@ export class EventsService extends Service {
|
|
|
55
54
|
public on<Event extends EventWithPayload>(event: Event, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
56
55
|
public on<Event extends EventWithPayload>(event: Event, priority: EventListenerPriority, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
57
56
|
public on<Event extends EventWithPayload>(event: Event, options: Partial<EventListenerOptions>, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
58
|
-
public on<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): () => void;
|
|
59
|
-
public on<Event extends string>(event: UnknownEvent<Event>, priority: EventListenerPriority, listener: EventListener): () => void; // prettier-ignore
|
|
60
|
-
public on<Event extends string>(event: UnknownEvent<Event>, options: Partial<EventListenerOptions>, listener: EventListener): () => void; // prettier-ignore
|
|
61
57
|
/* eslint-enable max-len */
|
|
62
58
|
|
|
63
59
|
public on(
|
|
@@ -83,8 +79,6 @@ export class EventsService extends Service {
|
|
|
83
79
|
public once<Event extends EventWithoutPayload>(event: Event, options: Partial<EventListenerOptions>, listener: () => unknown): () => void; // prettier-ignore
|
|
84
80
|
public once<Event extends EventWithPayload>(event: Event, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
85
81
|
public once<Event extends EventWithPayload>(event: Event, options: Partial<EventListenerOptions>, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
86
|
-
public once<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): () => void;
|
|
87
|
-
public once<Event extends string>(event: UnknownEvent<Event>, options: Partial<EventListenerOptions>, listener: EventListener): () => void; // prettier-ignore
|
|
88
82
|
/* eslint-enable max-len */
|
|
89
83
|
|
|
90
84
|
public once(
|
package/src/services/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import Events from './Events';
|
|
|
9
9
|
import Service from './Service';
|
|
10
10
|
import Storage from './Storage';
|
|
11
11
|
import { getPiniaStore } from './store';
|
|
12
|
+
import type { AppSetting } from './App.state';
|
|
12
13
|
|
|
13
14
|
export * from './App';
|
|
14
15
|
export * from './Cache';
|
|
@@ -53,6 +54,7 @@ export default definePlugin({
|
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
app.use(getPiniaStore());
|
|
57
|
+
options.settings?.forEach((setting) => App.addSetting(setting));
|
|
56
58
|
|
|
57
59
|
await bootServices(app, services);
|
|
58
60
|
},
|
|
@@ -61,6 +63,7 @@ export default definePlugin({
|
|
|
61
63
|
declare module '@aerogel/core/bootstrap/options' {
|
|
62
64
|
export interface AerogelOptions {
|
|
63
65
|
services?: Record<string, Service>;
|
|
66
|
+
settings?: AppSetting[];
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
|
package/src/ui/UI.state.ts
CHANGED
|
@@ -4,22 +4,16 @@ import { defineServiceState } from '@aerogel/core/services/Service';
|
|
|
4
4
|
|
|
5
5
|
import { Layouts, getCurrentLayout } from './utils';
|
|
6
6
|
|
|
7
|
-
export interface
|
|
7
|
+
export interface UIModal<T = unknown> {
|
|
8
8
|
id: string;
|
|
9
9
|
properties: Record<string, unknown>;
|
|
10
10
|
component: Component;
|
|
11
|
+
closing: boolean;
|
|
11
12
|
beforeClose: Promise<T | undefined>;
|
|
12
13
|
afterClose: Promise<T | undefined>;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export interface
|
|
16
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
17
|
-
Properties extends Record<string, unknown> = Record<string, unknown>,
|
|
18
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
19
|
-
Result = unknown,
|
|
20
|
-
> {}
|
|
21
|
-
|
|
22
|
-
export interface Snackbar {
|
|
16
|
+
export interface UIToast {
|
|
23
17
|
id: string;
|
|
24
18
|
component: Component;
|
|
25
19
|
properties: Record<string, unknown>;
|
|
@@ -28,12 +22,13 @@ export interface Snackbar {
|
|
|
28
22
|
export default defineServiceState({
|
|
29
23
|
name: 'ui',
|
|
30
24
|
initialState: {
|
|
31
|
-
modals: [] as
|
|
32
|
-
|
|
25
|
+
modals: [] as UIModal[],
|
|
26
|
+
toasts: [] as UIToast[],
|
|
33
27
|
layout: getCurrentLayout(),
|
|
34
28
|
},
|
|
35
29
|
computed: {
|
|
36
|
-
mobile: ({ layout }) => layout === Layouts.Mobile,
|
|
37
30
|
desktop: ({ layout }) => layout === Layouts.Desktop,
|
|
31
|
+
mobile: ({ layout }) => layout === Layouts.Mobile,
|
|
32
|
+
openModals: ({ modals }) => modals.filter(({ closing }) => !closing),
|
|
38
33
|
},
|
|
39
34
|
});
|
package/src/ui/UI.ts
CHANGED
|
@@ -1,52 +1,61 @@
|
|
|
1
1
|
import { after, facade, fail, isDevelopment, required, uuid } from '@noeldemartin/utils';
|
|
2
2
|
import { markRaw, nextTick } from 'vue';
|
|
3
|
+
import type { ComponentExposed, ComponentProps } from 'vue-component-type-helpers';
|
|
3
4
|
import type { Component } from 'vue';
|
|
4
|
-
import type {
|
|
5
|
+
import type { ClosureArgs } from '@noeldemartin/utils';
|
|
5
6
|
|
|
6
7
|
import App from '@aerogel/core/services/App';
|
|
7
8
|
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
9
|
import type {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
ConfirmModalCheckboxes,
|
|
11
|
+
ConfirmModalExpose,
|
|
12
|
+
ConfirmModalProps,
|
|
13
|
+
} from '@aerogel/core/components/contracts/ConfirmModal';
|
|
14
|
+
import type {
|
|
15
|
+
ErrorReportModalExpose,
|
|
16
|
+
ErrorReportModalProps,
|
|
17
|
+
} from '@aerogel/core/components/contracts/ErrorReportModal';
|
|
18
|
+
import type { AcceptRefs } from '@aerogel/core/utils';
|
|
19
|
+
import type { AlertModalExpose, AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
|
|
20
|
+
import type { ButtonVariant } from '@aerogel/core/components/contracts/Button';
|
|
21
|
+
import type { LoadingModalExpose, LoadingModalProps } from '@aerogel/core/components/contracts/LoadingModal';
|
|
22
|
+
import type { PromptModalExpose, PromptModalProps } from '@aerogel/core/components/contracts/PromptModal';
|
|
23
|
+
import type { ToastAction, ToastExpose, ToastProps, ToastVariant } from '@aerogel/core/components/contracts/Toast';
|
|
17
24
|
|
|
18
25
|
import Service from './UI.state';
|
|
19
26
|
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
20
|
-
import type {
|
|
27
|
+
import type { UIModal, UIToast } from './UI.state';
|
|
21
28
|
|
|
22
29
|
interface ModalCallbacks<T = unknown> {
|
|
23
30
|
willClose(result: T | undefined): void;
|
|
24
|
-
|
|
31
|
+
hasClosed(result: T | undefined): void;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
type
|
|
28
|
-
type
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export type UIComponent = ObjectValues<typeof UIComponents>;
|
|
34
|
+
export type ModalResult<T> = ModalExposeResult<ComponentExposed<T>>;
|
|
35
|
+
export type ModalExposeResult<T> = T extends { close(result?: infer Result): Promise<void> } ? Result : unknown;
|
|
36
|
+
export type UIComponent<Props = {}, Exposed = {}> = { new (...args: ClosureArgs): Exposed & { $props: Props } };
|
|
37
|
+
|
|
38
|
+
export interface UIComponents {
|
|
39
|
+
'alert-modal': UIComponent<AlertModalProps, AlertModalExpose>;
|
|
40
|
+
'confirm-modal': UIComponent<ConfirmModalProps, ConfirmModalExpose>;
|
|
41
|
+
'error-report-modal': UIComponent<ErrorReportModalProps, ErrorReportModalExpose>;
|
|
42
|
+
'loading-modal': UIComponent<LoadingModalProps, LoadingModalExpose>;
|
|
43
|
+
'prompt-modal': UIComponent<PromptModalProps, PromptModalExpose>;
|
|
44
|
+
'router-link': UIComponent;
|
|
45
|
+
'startup-crash': UIComponent;
|
|
46
|
+
toast: UIComponent<ToastProps, ToastExpose>;
|
|
47
|
+
}
|
|
42
48
|
|
|
43
|
-
export
|
|
49
|
+
export interface UIModalContext {
|
|
50
|
+
modal: UIModal;
|
|
51
|
+
childIndex?: number;
|
|
52
|
+
}
|
|
44
53
|
|
|
45
54
|
export type ConfirmOptions = AcceptRefs<{
|
|
46
55
|
acceptText?: string;
|
|
47
|
-
|
|
56
|
+
acceptVariant?: ButtonVariant;
|
|
48
57
|
cancelText?: string;
|
|
49
|
-
|
|
58
|
+
cancelVariant?: ButtonVariant;
|
|
50
59
|
actions?: Record<string, () => unknown>;
|
|
51
60
|
required?: boolean;
|
|
52
61
|
}>;
|
|
@@ -57,7 +66,8 @@ export type LoadingOptions = AcceptRefs<{
|
|
|
57
66
|
progress?: number;
|
|
58
67
|
}>;
|
|
59
68
|
|
|
60
|
-
export interface ConfirmOptionsWithCheckboxes<T extends
|
|
69
|
+
export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
|
|
70
|
+
extends ConfirmOptions {
|
|
61
71
|
checkboxes?: T;
|
|
62
72
|
}
|
|
63
73
|
|
|
@@ -66,31 +76,39 @@ export type PromptOptions = AcceptRefs<{
|
|
|
66
76
|
defaultValue?: string;
|
|
67
77
|
placeholder?: string;
|
|
68
78
|
acceptText?: string;
|
|
69
|
-
|
|
79
|
+
acceptVariant?: ButtonVariant;
|
|
70
80
|
cancelText?: string;
|
|
71
|
-
|
|
81
|
+
cancelVariant?: ButtonVariant;
|
|
72
82
|
trim?: boolean;
|
|
73
83
|
}>;
|
|
74
84
|
|
|
75
|
-
export interface
|
|
85
|
+
export interface ToastOptions {
|
|
76
86
|
component?: Component;
|
|
77
|
-
|
|
78
|
-
actions?:
|
|
87
|
+
variant?: ToastVariant;
|
|
88
|
+
actions?: ToastAction[];
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
export class UIService extends Service {
|
|
82
92
|
|
|
83
93
|
private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
|
|
84
|
-
private components: Partial<
|
|
94
|
+
private components: Partial<UIComponents> = {};
|
|
95
|
+
|
|
96
|
+
public registerComponent<T extends keyof UIComponents>(name: T, component: UIComponents[T]): void {
|
|
97
|
+
this.components[name] = component;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public resolveComponent<T extends keyof UIComponents>(name: T): UIComponents[T] | null {
|
|
101
|
+
return this.components[name] ?? null;
|
|
102
|
+
}
|
|
85
103
|
|
|
86
|
-
public requireComponent(name:
|
|
87
|
-
return this.
|
|
104
|
+
public requireComponent<T extends keyof UIComponents>(name: T): UIComponents[T] {
|
|
105
|
+
return this.resolveComponent(name) ?? fail(`UI Component '${name}' is not defined!`);
|
|
88
106
|
}
|
|
89
107
|
|
|
90
108
|
public alert(message: string): void;
|
|
91
109
|
public alert(title: string, message: string): void;
|
|
92
110
|
public alert(messageOrTitle: string, message?: string): void {
|
|
93
|
-
const getProperties = ():
|
|
111
|
+
const getProperties = (): AlertModalProps => {
|
|
94
112
|
if (typeof message !== 'string') {
|
|
95
113
|
return { message: messageOrTitle };
|
|
96
114
|
}
|
|
@@ -101,14 +119,14 @@ export class UIService extends Service {
|
|
|
101
119
|
};
|
|
102
120
|
};
|
|
103
121
|
|
|
104
|
-
this.
|
|
122
|
+
this.modal(this.requireComponent('alert-modal'), getProperties());
|
|
105
123
|
}
|
|
106
124
|
|
|
107
125
|
/* eslint-disable max-len */
|
|
108
126
|
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
109
127
|
public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
110
|
-
public async confirm<T extends
|
|
111
|
-
public async confirm<T extends
|
|
128
|
+
public async confirm<T extends ConfirmModalCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
129
|
+
public async confirm<T extends ConfirmModalCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
112
130
|
/* eslint-enable max-len */
|
|
113
131
|
|
|
114
132
|
public async confirm(
|
|
@@ -116,7 +134,7 @@ export class UIService extends Service {
|
|
|
116
134
|
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
117
135
|
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
118
136
|
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
119
|
-
const getProperties = ():
|
|
137
|
+
const getProperties = (): AcceptRefs<ConfirmModalProps> => {
|
|
120
138
|
if (typeof messageOrOptions !== 'string') {
|
|
121
139
|
return {
|
|
122
140
|
...(messageOrOptions ?? {}),
|
|
@@ -132,12 +150,9 @@ export class UIService extends Service {
|
|
|
132
150
|
required: !!options?.required,
|
|
133
151
|
};
|
|
134
152
|
};
|
|
135
|
-
const properties = getProperties();
|
|
136
|
-
const modal = await this.openModal<
|
|
137
|
-
ModalComponent<AGConfirmModalProps, boolean | [boolean, Record<string, boolean>]>
|
|
138
|
-
>(this.requireComponent(UIComponents.ConfirmModal), properties);
|
|
139
|
-
const result = await modal.beforeClose;
|
|
140
153
|
|
|
154
|
+
const properties = getProperties();
|
|
155
|
+
const result = await this.modalForm(this.requireComponent('confirm-modal'), properties);
|
|
141
156
|
const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
|
|
142
157
|
const checkboxes =
|
|
143
158
|
typeof result === 'object'
|
|
@@ -174,26 +189,22 @@ export class UIService extends Service {
|
|
|
174
189
|
options?: PromptOptions,
|
|
175
190
|
): Promise<string | null> {
|
|
176
191
|
const trim = options?.trim ?? true;
|
|
177
|
-
const getProperties = ():
|
|
192
|
+
const getProperties = (): PromptModalProps => {
|
|
178
193
|
if (typeof messageOrOptions !== 'string') {
|
|
179
194
|
return {
|
|
180
195
|
message: messageOrTitle,
|
|
181
196
|
...(messageOrOptions ?? {}),
|
|
182
|
-
} as
|
|
197
|
+
} as PromptModalProps;
|
|
183
198
|
}
|
|
184
199
|
|
|
185
200
|
return {
|
|
186
201
|
title: messageOrTitle,
|
|
187
202
|
message: messageOrOptions,
|
|
188
203
|
...(options ?? {}),
|
|
189
|
-
} as
|
|
204
|
+
} as PromptModalProps;
|
|
190
205
|
};
|
|
191
206
|
|
|
192
|
-
const
|
|
193
|
-
this.requireComponent(UIComponents.PromptModal),
|
|
194
|
-
getProperties(),
|
|
195
|
-
);
|
|
196
|
-
const rawResult = await modal.beforeClose;
|
|
207
|
+
const rawResult = await this.modalForm(this.requireComponent('prompt-modal'), getProperties());
|
|
197
208
|
const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
|
|
198
209
|
|
|
199
210
|
return result ?? null;
|
|
@@ -207,7 +218,7 @@ export class UIService extends Service {
|
|
|
207
218
|
operation?: Promise<T> | (() => T),
|
|
208
219
|
): Promise<T> {
|
|
209
220
|
const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
|
|
210
|
-
const processArgs = (): { operationPromise: Promise<T>; props?:
|
|
221
|
+
const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
|
|
211
222
|
if (typeof operationOrMessageOrOptions === 'string') {
|
|
212
223
|
return {
|
|
213
224
|
props: { message: operationOrMessageOrOptions },
|
|
@@ -226,10 +237,12 @@ export class UIService extends Service {
|
|
|
226
237
|
};
|
|
227
238
|
|
|
228
239
|
const { operationPromise, props } = processArgs();
|
|
229
|
-
const modal = await this.
|
|
240
|
+
const modal = await this.modal(this.requireComponent('loading-modal'), props);
|
|
230
241
|
|
|
231
242
|
try {
|
|
232
|
-
const
|
|
243
|
+
const result = await operationPromise;
|
|
244
|
+
|
|
245
|
+
await after({ ms: 500 });
|
|
233
246
|
|
|
234
247
|
return result;
|
|
235
248
|
} finally {
|
|
@@ -237,43 +250,34 @@ export class UIService extends Service {
|
|
|
237
250
|
}
|
|
238
251
|
}
|
|
239
252
|
|
|
240
|
-
public
|
|
241
|
-
const
|
|
253
|
+
public toast(message: string, options: ToastOptions = {}): void {
|
|
254
|
+
const { component, ...otherOptions } = options;
|
|
255
|
+
const toast: UIToast = {
|
|
242
256
|
id: uuid(),
|
|
243
|
-
properties: { message, ...
|
|
244
|
-
component: markRaw(
|
|
257
|
+
properties: { message, ...otherOptions },
|
|
258
|
+
component: markRaw(component ?? this.requireComponent('toast')),
|
|
245
259
|
};
|
|
246
260
|
|
|
247
|
-
this.setState('
|
|
248
|
-
|
|
249
|
-
setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
public hideSnackbar(id: string): void {
|
|
253
|
-
this.setState(
|
|
254
|
-
'snackbars',
|
|
255
|
-
this.snackbars.filter((snackbar) => snackbar.id !== id),
|
|
256
|
-
);
|
|
261
|
+
this.setState('toasts', this.toasts.concat(toast));
|
|
257
262
|
}
|
|
258
263
|
|
|
259
|
-
public
|
|
260
|
-
|
|
261
|
-
|
|
264
|
+
public modal<T extends Component>(
|
|
265
|
+
...args: {} extends ComponentProps<T>
|
|
266
|
+
? [component: T, props?: AcceptRefs<ComponentProps<T>>]
|
|
267
|
+
: [component: T, props: AcceptRefs<ComponentProps<T>>]
|
|
268
|
+
): Promise<UIModal<ModalResult<T>>>;
|
|
262
269
|
|
|
263
|
-
public async
|
|
264
|
-
component: TModalComponent,
|
|
265
|
-
properties?: ModalProperties<TModalComponent>,
|
|
266
|
-
): Promise<Modal<ModalResult<TModalComponent>>> {
|
|
270
|
+
public async modal<T extends Component>(component: T, props?: ComponentProps<T>): Promise<UIModal<ModalResult<T>>> {
|
|
267
271
|
const id = uuid();
|
|
268
|
-
const callbacks: Partial<ModalCallbacks<ModalResult<
|
|
269
|
-
const modal:
|
|
272
|
+
const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
|
|
273
|
+
const modal: UIModal<ModalResult<T>> = {
|
|
270
274
|
id,
|
|
271
|
-
|
|
275
|
+
closing: false,
|
|
276
|
+
properties: props ?? {},
|
|
272
277
|
component: markRaw(component),
|
|
273
278
|
beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
|
|
274
|
-
afterClose: new Promise((resolve) => (callbacks.
|
|
279
|
+
afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
|
|
275
280
|
};
|
|
276
|
-
const activeModal = this.modals.at(-1);
|
|
277
281
|
const modals = this.modals.concat(modal);
|
|
278
282
|
|
|
279
283
|
this.modalCallbacks[modal.id] = callbacks;
|
|
@@ -281,15 +285,26 @@ export class UIService extends Service {
|
|
|
281
285
|
this.setState({ modals });
|
|
282
286
|
|
|
283
287
|
await nextTick();
|
|
284
|
-
await (activeModal && Events.emit('hide-modal', { id: activeModal.id }));
|
|
285
|
-
await Promise.all([
|
|
286
|
-
activeModal || Events.emit('show-overlays-backdrop'),
|
|
287
|
-
Events.emit('show-modal', { id: modal.id }),
|
|
288
|
-
]);
|
|
289
288
|
|
|
290
289
|
return modal;
|
|
291
290
|
}
|
|
292
291
|
|
|
292
|
+
public modalForm<T extends Component>(
|
|
293
|
+
...args: {} extends ComponentProps<T>
|
|
294
|
+
? [component: T, props?: AcceptRefs<ComponentProps<T>>]
|
|
295
|
+
: [component: T, props: AcceptRefs<ComponentProps<T>>]
|
|
296
|
+
): Promise<ModalResult<T> | undefined>;
|
|
297
|
+
|
|
298
|
+
public async modalForm<T extends Component>(
|
|
299
|
+
component: T,
|
|
300
|
+
props?: ComponentProps<T>,
|
|
301
|
+
): Promise<ModalResult<T> | undefined> {
|
|
302
|
+
const modal = await this.modal<T>(component, props as ComponentProps<T>);
|
|
303
|
+
const result = await modal.beforeClose;
|
|
304
|
+
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
293
308
|
public async closeModal(id: string, result?: unknown): Promise<void> {
|
|
294
309
|
if (!App.isMounted()) {
|
|
295
310
|
await this.removeModal(id, result);
|
|
@@ -318,25 +333,23 @@ export class UIService extends Service {
|
|
|
318
333
|
this.modals.filter((m) => m.id !== id),
|
|
319
334
|
);
|
|
320
335
|
|
|
321
|
-
this.modalCallbacks[id]?.
|
|
336
|
+
this.modalCallbacks[id]?.hasClosed?.(result);
|
|
322
337
|
|
|
323
338
|
delete this.modalCallbacks[id];
|
|
324
|
-
|
|
325
|
-
const activeModal = this.modals.at(-1);
|
|
326
|
-
|
|
327
|
-
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
328
339
|
}
|
|
329
340
|
|
|
330
341
|
private watchModalEvents(): void {
|
|
331
|
-
Events.on('modal-will-close', ({ modal, result }) => {
|
|
332
|
-
this.
|
|
342
|
+
Events.on('modal-will-close', ({ modal: { id }, result }) => {
|
|
343
|
+
const modal = this.modals.find((_modal) => id === _modal.id);
|
|
333
344
|
|
|
334
|
-
if (
|
|
335
|
-
|
|
345
|
+
if (modal) {
|
|
346
|
+
modal.closing = true;
|
|
336
347
|
}
|
|
348
|
+
|
|
349
|
+
this.modalCallbacks[id]?.willClose?.(result);
|
|
337
350
|
});
|
|
338
351
|
|
|
339
|
-
Events.on('modal-closed', async ({ modal: { id }, result }) => {
|
|
352
|
+
Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
|
|
340
353
|
await this.removeModal(id, result);
|
|
341
354
|
});
|
|
342
355
|
}
|
|
@@ -380,11 +393,7 @@ export default facade(UIService);
|
|
|
380
393
|
declare module '@aerogel/core/services/Events' {
|
|
381
394
|
export interface EventsPayload {
|
|
382
395
|
'close-modal': { id: string; result?: unknown };
|
|
383
|
-
'
|
|
384
|
-
'
|
|
385
|
-
'modal-closed': { modal: Modal; result?: unknown };
|
|
386
|
-
'modal-will-close': { modal: Modal; result?: unknown };
|
|
387
|
-
'show-modal': { id: string };
|
|
388
|
-
'show-overlays-backdrop': void;
|
|
396
|
+
'modal-will-close': { modal: UIModal; result?: unknown };
|
|
397
|
+
'modal-has-closed': { modal: UIModal; result?: unknown };
|
|
389
398
|
}
|
|
390
399
|
}
|