@aerogel/core 0.0.0-next.f1f5a990033d966dc0bb12d251110fbc9350dcc7 → 0.0.0-next.fd1bd21aea7a9ab8c4eab69a5f5776db5de8bf35

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.
Files changed (52) hide show
  1. package/dist/aerogel-core.cjs.js +1 -1
  2. package/dist/aerogel-core.cjs.js.map +1 -1
  3. package/dist/aerogel-core.d.ts +511 -119
  4. package/dist/aerogel-core.esm.js +1 -1
  5. package/dist/aerogel-core.esm.js.map +1 -1
  6. package/package.json +2 -2
  7. package/src/bootstrap/index.ts +6 -2
  8. package/src/bootstrap/options.ts +3 -0
  9. package/src/components/forms/AGCheckbox.vue +7 -1
  10. package/src/components/forms/AGForm.vue +9 -10
  11. package/src/components/forms/AGInput.vue +10 -6
  12. package/src/components/forms/AGSelect.story.vue +21 -3
  13. package/src/components/forms/AGSelect.vue +10 -3
  14. package/src/components/headless/forms/AGHeadlessButton.vue +17 -12
  15. package/src/components/headless/forms/AGHeadlessInput.ts +7 -3
  16. package/src/components/headless/forms/AGHeadlessInput.vue +3 -3
  17. package/src/components/headless/forms/AGHeadlessInputDescription.vue +28 -0
  18. package/src/components/headless/forms/AGHeadlessInputInput.vue +40 -4
  19. package/src/components/headless/forms/AGHeadlessInputTextArea.vue +40 -0
  20. package/src/components/headless/forms/AGHeadlessSelect.ts +15 -12
  21. package/src/components/headless/forms/AGHeadlessSelect.vue +23 -22
  22. package/src/components/headless/forms/AGHeadlessSelectOption.vue +6 -6
  23. package/src/components/headless/forms/composition.ts +10 -0
  24. package/src/components/headless/forms/index.ts +3 -0
  25. package/src/components/lib/AGErrorMessage.vue +2 -2
  26. package/src/components/lib/AGMarkdown.vue +7 -2
  27. package/src/components/modals/AGPromptModal.ts +30 -0
  28. package/src/components/modals/AGPromptModal.vue +34 -0
  29. package/src/components/modals/index.ts +10 -19
  30. package/src/directives/index.ts +2 -0
  31. package/src/directives/measure.ts +11 -2
  32. package/src/errors/Errors.ts +16 -19
  33. package/src/errors/index.ts +1 -10
  34. package/src/errors/utils.ts +19 -0
  35. package/src/forms/Form.ts +43 -3
  36. package/src/forms/index.ts +1 -0
  37. package/src/forms/utils.ts +15 -0
  38. package/src/jobs/Job.ts +5 -0
  39. package/src/jobs/index.ts +7 -0
  40. package/src/lang/Lang.ts +11 -23
  41. package/src/main.ts +3 -0
  42. package/src/services/App.state.ts +1 -2
  43. package/src/services/App.ts +21 -3
  44. package/src/services/Cache.ts +43 -0
  45. package/src/services/Events.test.ts +39 -0
  46. package/src/services/Events.ts +100 -30
  47. package/src/services/Service.ts +42 -12
  48. package/src/services/index.ts +4 -1
  49. package/src/services/store.ts +8 -5
  50. package/src/testing/index.ts +25 -0
  51. package/src/ui/UI.ts +51 -6
  52. package/src/ui/index.ts +2 -0
@@ -9,31 +9,31 @@
9
9
  [unselectedClass ?? 'unselected']: !selected,
10
10
  }"
11
11
  >
12
- {{ option?.text }}
12
+ {{ select.renderText(value) }}
13
13
  </li>
14
14
  </slot>
15
15
  </ListboxOption>
16
16
  </template>
17
17
 
18
18
  <script setup lang="ts">
19
- import { computed } from 'vue';
20
19
  import { ListboxOption } from '@headlessui/vue';
21
20
 
22
21
  import { injectReactiveOrFail, requiredMixedProp, stringProp } from '@/utils/vue';
23
22
  import type { ComponentProps } from '@/utils/vue';
23
+ import type { FormFieldValue } from '@/forms/Form';
24
24
 
25
- import type { IAGHeadlessSelect, IAGSelectOptionValue } from './AGHeadlessSelect';
25
+ import type { IAGHeadlessSelect } from './AGHeadlessSelect';
26
26
 
27
- const props = defineProps({
28
- value: requiredMixedProp<IAGSelectOptionValue>(),
27
+ defineProps({
28
+ value: requiredMixedProp<FormFieldValue>(),
29
29
  selectedClass: stringProp(),
30
30
  unselectedClass: stringProp(),
31
31
  activeClass: stringProp(),
32
32
  inactiveClass: stringProp(),
33
33
  });
34
+
34
35
  const select = injectReactiveOrFail<IAGHeadlessSelect>(
35
36
  'select',
36
37
  '<AGHeadlessSelectOption> must be a child of a <AGHeadlessSelect>',
37
38
  );
38
- const option = computed(() => select.options.find((selectOption) => selectOption.value === props.value));
39
39
  </script>
@@ -0,0 +1,10 @@
1
+ import { inject, onUnmounted } from 'vue';
2
+
3
+ import type Form from '@/forms/Form';
4
+
5
+ export function onFormFocus(input: { name: string | null }, listener: () => unknown): void {
6
+ const form = inject<Form | null>('form', null);
7
+ const stop = form?.on('focus', (name) => input.name === name && listener());
8
+
9
+ onUnmounted(() => stop?.());
10
+ }
@@ -1,11 +1,14 @@
1
+ export * from './composition';
1
2
  export * from './AGHeadlessInput';
2
3
  export * from './AGHeadlessSelect';
3
4
  export * from './AGHeadlessSelectOption';
4
5
  export { default as AGHeadlessButton } from './AGHeadlessButton.vue';
5
6
  export { default as AGHeadlessInput } from './AGHeadlessInput.vue';
7
+ export { default as AGHeadlessInputDescription } from './AGHeadlessInputDescription.vue';
6
8
  export { default as AGHeadlessInputError } from './AGHeadlessInputError.vue';
7
9
  export { default as AGHeadlessInputInput } from './AGHeadlessInputInput.vue';
8
10
  export { default as AGHeadlessInputLabel } from './AGHeadlessInputLabel.vue';
11
+ export { default as AGHeadlessInputTextArea } from './AGHeadlessInputTextArea.vue';
9
12
  export { default as AGHeadlessSelect } from './AGHeadlessSelect.vue';
10
13
  export { default as AGHeadlessSelectButton } from './AGHeadlessSelectButton.vue';
11
14
  export { default as AGHeadlessSelectError } from './AGHeadlessSelectError.vue';
@@ -5,12 +5,12 @@
5
5
  <script setup lang="ts">
6
6
  import { computed } from 'vue';
7
7
 
8
- import Errors from '@/errors/Errors';
9
8
  import { requiredObjectProp } from '@/utils/vue';
9
+ import { getErrorMessage } from '@/errors/utils';
10
10
  import type { ErrorSource } from '@/errors/Errors.state';
11
11
 
12
12
  import AGMarkdown from './AGMarkdown.vue';
13
13
 
14
14
  const props = defineProps({ error: requiredObjectProp<ErrorSource>() });
15
- const message = computed(() => Errors.getErrorMessage(props.error));
15
+ const message = computed(() => getErrorMessage(props.error));
16
16
  </script>
@@ -3,7 +3,7 @@
3
3
  </template>
4
4
 
5
5
  <script setup lang="ts">
6
- import { computed, h } from 'vue';
6
+ import { computed, h, useAttrs } from 'vue';
7
7
 
8
8
  import { renderMarkdown } from '@/utils/markdown';
9
9
  import { booleanProp, objectProp, stringProp } from '@/utils/vue';
@@ -17,6 +17,7 @@ const props = defineProps({
17
17
  text: stringProp(),
18
18
  });
19
19
 
20
+ const attrs = useAttrs();
20
21
  const markdown = computed(() => props.text ?? (props.langKey && translate(props.langKey, props.langParams ?? {})));
21
22
  const html = computed(() => {
22
23
  if (!markdown.value) {
@@ -32,5 +33,9 @@ const html = computed(() => {
32
33
  return renderedHtml;
33
34
  });
34
35
  const root = () =>
35
- h(props.as ?? (props.inline ? 'span' : 'div'), { class: props.inline ? '' : 'prose', innerHTML: html.value });
36
+ h(props.as ?? (props.inline ? 'span' : 'div'), {
37
+ innerHTML: html.value,
38
+ ...attrs,
39
+ class: `${attrs.class ?? ''} ${props.inline ? '' : 'prose'}`,
40
+ });
36
41
  </script>
@@ -0,0 +1,30 @@
1
+ import { computed } from 'vue';
2
+ import type { ExtractPropTypes } from 'vue';
3
+ import type { ObjectWithoutEmpty } from '@noeldemartin/utils';
4
+
5
+ import { requiredStringProp, stringProp } from '@/utils';
6
+ import { translateWithDefault } from '@/lang';
7
+
8
+ export const promptModalProps = {
9
+ title: stringProp(),
10
+ message: requiredStringProp(),
11
+ label: stringProp(),
12
+ defaultValue: stringProp(),
13
+ placeholder: stringProp(),
14
+ acceptText: stringProp(),
15
+ cancelText: stringProp(),
16
+ };
17
+
18
+ export type AGPromptModalProps = ObjectWithoutEmpty<ExtractPropTypes<typeof promptModalProps>>;
19
+
20
+ export function usePromptModalProps(): typeof promptModalProps {
21
+ return promptModalProps;
22
+ }
23
+
24
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
25
+ export function usePromptModal(props: ExtractPropTypes<typeof promptModalProps>) {
26
+ const renderedAcceptText = computed(() => props.acceptText ?? translateWithDefault('ui.accept', 'Ok'));
27
+ const renderedCancelText = computed(() => props.cancelText ?? translateWithDefault('ui.cancel', 'Cancel'));
28
+
29
+ return { renderedAcceptText, renderedCancelText };
30
+ }
@@ -0,0 +1,34 @@
1
+ <template>
2
+ <AGModal v-slot="{ close }: IAGModalDefaultSlotProps" :cancellable="false" :title="title">
3
+ <AGMarkdown :text="message" />
4
+
5
+ <AGForm :form="form" @submit="close(form.draft)">
6
+ <AGInput name="draft" :placeholder="placeholder" :label="label" />
7
+
8
+ <div class="mt-2 flex flex-row-reverse gap-2">
9
+ <AGButton submit>
10
+ {{ renderedAcceptText }}
11
+ </AGButton>
12
+ <AGButton color="secondary" @click="close()">
13
+ {{ renderedCancelText }}
14
+ </AGButton>
15
+ </div>
16
+ </AGForm>
17
+ </AGModal>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ import AGModal from './AGModal.vue';
22
+ import { usePromptModal, usePromptModalProps } from './AGPromptModal';
23
+ import type { IAGModalDefaultSlotProps } from './AGModal';
24
+
25
+ import AGButton from '../forms/AGButton.vue';
26
+ import AGForm from '../forms/AGForm.vue';
27
+ import AGInput from '../forms/AGInput.vue';
28
+ import AGMarkdown from '../lib/AGMarkdown.vue';
29
+ import { requiredStringInput, useForm } from '../../forms';
30
+
31
+ const props = defineProps(usePromptModalProps());
32
+ const form = useForm({ draft: requiredStringInput(props.defaultValue ?? '') });
33
+ const { renderedAcceptText, renderedCancelText } = usePromptModal(props);
34
+ </script>
@@ -1,26 +1,17 @@
1
- import AGAlertModal from './AGAlertModal.vue';
2
- import AGConfirmModal from './AGConfirmModal.vue';
3
- import AGErrorReportModalButtons from './AGErrorReportModalButtons.vue';
4
- import AGErrorReportModalTitle from './AGErrorReportModalTitle.vue';
5
- import AGLoadingModal from './AGLoadingModal.vue';
6
- import AGModal from './AGModal.vue';
7
- import AGModalTitle from './AGModalTitle.vue';
8
- import AGModalContext from './AGModalContext.vue';
9
-
10
1
  export * from './AGAlertModal';
11
2
  export * from './AGConfirmModal';
12
3
  export * from './AGErrorReportModal';
13
4
  export * from './AGLoadingModal';
14
5
  export * from './AGModal';
15
6
  export * from './AGModalContext';
7
+ export * from './AGPromptModal';
16
8
 
17
- export {
18
- AGAlertModal,
19
- AGConfirmModal,
20
- AGErrorReportModalButtons,
21
- AGErrorReportModalTitle,
22
- AGLoadingModal,
23
- AGModal,
24
- AGModalTitle,
25
- AGModalContext,
26
- };
9
+ export { default as AGAlertModal } from './AGAlertModal.vue';
10
+ export { default as AGConfirmModal } from './AGConfirmModal.vue';
11
+ export { default as AGErrorReportModalButtons } from './AGErrorReportModalButtons.vue';
12
+ export { default as AGErrorReportModalTitle } from './AGErrorReportModalTitle.vue';
13
+ export { default as AGLoadingModal } from './AGLoadingModal.vue';
14
+ export { default as AGModal } from './AGModal.vue';
15
+ export { default as AGModalContext } from './AGModalContext.vue';
16
+ export { default as AGModalTitle } from './AGModalTitle.vue';
17
+ export { default as AGPromptModal } from './AGPromptModal.vue';
@@ -10,6 +10,8 @@ const builtInDirectives: Record<string, Directive> = {
10
10
  'measure': measure,
11
11
  };
12
12
 
13
+ export * from './measure';
14
+
13
15
  export default definePlugin({
14
16
  install(app, options) {
15
17
  const directives = {
@@ -1,12 +1,21 @@
1
1
  import { defineDirective } from '@/utils/vue';
2
2
 
3
+ export interface ElementSize {
4
+ width: number;
5
+ height: number;
6
+ }
7
+
8
+ export type MeasureDirectiveListener = (size: ElementSize) => unknown;
9
+
3
10
  export default defineDirective({
4
- mounted(element: HTMLElement, { value }: { value?: () => unknown }) {
11
+ mounted(element: HTMLElement, { value }) {
12
+ const listener = typeof value === 'function' ? (value as MeasureDirectiveListener) : null;
5
13
  const sizes = element.getBoundingClientRect();
6
14
 
15
+ // TODO guard with modifiers.css once typed properly
7
16
  element.style.setProperty('--width', `${sizes.width}px`);
8
17
  element.style.setProperty('--height', `${sizes.height}px`);
9
18
 
10
- value?.();
19
+ listener?.({ width: sizes.width, height: sizes.height });
11
20
  },
12
21
  });
@@ -7,6 +7,7 @@ import { translateWithDefault } from '@/lang/utils';
7
7
 
8
8
  import Service from './Errors.state';
9
9
  import { Colors } from '@/components/constants';
10
+ import { Events } from '@/services';
10
11
  import type { AGErrorReportModalProps } from '@/components/modals/AGErrorReportModal';
11
12
  import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
12
13
  import type { ModalComponent } from '@/ui/UI.state';
@@ -39,7 +40,13 @@ export class ErrorsService extends Service {
39
40
  }
40
41
 
41
42
  public async report(error: ErrorSource, message?: string): Promise<void> {
42
- if (App.development || App.testing) {
43
+ await Events.emit('error', { error, message });
44
+
45
+ if (App.testing) {
46
+ throw error;
47
+ }
48
+
49
+ if (App.development) {
43
50
  this.logError(error);
44
51
  }
45
52
 
@@ -47,7 +54,7 @@ export class ErrorsService extends Service {
47
54
  throw error;
48
55
  }
49
56
 
50
- if (!App.isMounted) {
57
+ if (!App.isMounted()) {
51
58
  const startupError = await this.createStartupErrorReport(error);
52
59
 
53
60
  if (startupError) {
@@ -110,22 +117,6 @@ export class ErrorsService extends Service {
110
117
  });
111
118
  }
112
119
 
113
- public getErrorMessage(error: ErrorSource): string {
114
- if (typeof error === 'string') {
115
- return error;
116
- }
117
-
118
- if (error instanceof Error || error instanceof JSError) {
119
- return error.message;
120
- }
121
-
122
- if (isObject(error)) {
123
- return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
124
- }
125
-
126
- return translateWithDefault('errors.unknown', 'Unknown Error');
127
- }
128
-
129
120
  private logError(error: unknown): void {
130
121
  // eslint-disable-next-line no-console
131
122
  console.error(error);
@@ -185,4 +176,10 @@ export class ErrorsService extends Service {
185
176
 
186
177
  }
187
178
 
188
- export default facade(new ErrorsService());
179
+ export default facade(ErrorsService);
180
+
181
+ declare module '@/services/Events' {
182
+ export interface EventsPayload {
183
+ error: { error: ErrorSource; message?: string };
184
+ }
185
+ }
@@ -6,20 +6,11 @@ import { definePlugin } from '@/plugins';
6
6
  import Errors from './Errors';
7
7
  import { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
8
8
 
9
+ export * from './utils';
9
10
  export { Errors, ErrorSource, ErrorReport, ErrorReportLog };
10
11
 
11
12
  const services = { $errors: Errors };
12
13
  const frameworkHandler: ErrorHandler = (error) => {
13
- if (!Errors.instance) {
14
- // eslint-disable-next-line no-console
15
- console.warn('Errors service hasn\'t been initialized properly!');
16
-
17
- // eslint-disable-next-line no-console
18
- console.error(error);
19
-
20
- return true;
21
- }
22
-
23
14
  Errors.report(error);
24
15
 
25
16
  return true;
@@ -0,0 +1,19 @@
1
+ import { JSError, isObject, toString } from '@noeldemartin/utils';
2
+ import { translateWithDefault } from '@/lang/utils';
3
+ import type { ErrorSource } from './Errors.state';
4
+
5
+ export function getErrorMessage(error: ErrorSource): string {
6
+ if (typeof error === 'string') {
7
+ return error;
8
+ }
9
+
10
+ if (error instanceof Error || error instanceof JSError) {
11
+ return error.message;
12
+ }
13
+
14
+ if (isObject(error)) {
15
+ return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
16
+ }
17
+
18
+ return translateWithDefault('errors.unknown', 'Unknown Error');
19
+ }
package/src/forms/Form.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { MagicObject } from '@noeldemartin/utils';
2
- import { computed, reactive, readonly, ref } from 'vue';
1
+ import { MagicObject, arrayRemove } from '@noeldemartin/utils';
2
+ import { computed, nextTick, reactive, readonly, ref } from 'vue';
3
3
  import type { ObjectValues } from '@noeldemartin/utils';
4
4
  import type { ComputedRef, DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
5
5
 
@@ -8,6 +8,7 @@ export const FormFieldTypes = {
8
8
  Number: 'number',
9
9
  Boolean: 'boolean',
10
10
  Object: 'object',
11
+ Date: 'date',
11
12
  } as const;
12
13
 
13
14
  export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
@@ -18,6 +19,7 @@ export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType
18
19
 
19
20
  export type FormFieldDefinitions = Record<string, FormFieldDefinition>;
20
21
  export type FormFieldType = ObjectValues<typeof FormFieldTypes>;
22
+ export type FormFieldValue = GetFormFieldValue<FormFieldType>;
21
23
 
22
24
  export type FormData<T> = {
23
25
  -readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
@@ -39,10 +41,15 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
39
41
  ? boolean
40
42
  : TType extends typeof FormFieldTypes.Object
41
43
  ? object
44
+ : TType extends typeof FormFieldTypes.Date
45
+ ? Date
42
46
  : never;
43
47
 
44
48
  const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
45
49
 
50
+ export type SubmitFormListener = () => unknown;
51
+ export type FocusFormListener = (input: string) => unknown;
52
+
46
53
  export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
47
54
 
48
55
  public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
@@ -51,6 +58,7 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
51
58
  private _data: FormData<Fields>;
52
59
  private _submitted: Ref<boolean>;
53
60
  private _errors: FormErrors<Fields>;
61
+ private _listeners: { focus?: FocusFormListener[]; submit?: SubmitFormListener[] } = {};
54
62
 
55
63
  constructor(fields: Fields) {
56
64
  super();
@@ -88,6 +96,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
88
96
  return this._data[field] as unknown as GetFormFieldValue<Fields[T]['type']>;
89
97
  }
90
98
 
99
+ public data(): FormData<Fields> {
100
+ return { ...this._data };
101
+ }
102
+
91
103
  public validate(): boolean {
92
104
  const errors = Object.entries(this._fields).reduce((formErrors, [name, definition]) => {
93
105
  formErrors[name] = this.getFieldErrors(name, definition);
@@ -110,7 +122,35 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
110
122
  public submit(): boolean {
111
123
  this._submitted.value = true;
112
124
 
113
- return this.validate();
125
+ const valid = this.validate();
126
+
127
+ valid && this._listeners['submit']?.forEach((listener) => listener());
128
+
129
+ return valid;
130
+ }
131
+
132
+ public on(event: 'focus', listener: FocusFormListener): () => void;
133
+ public on(event: 'submit', listener: SubmitFormListener): () => void;
134
+ public on(event: 'focus' | 'submit', listener: FocusFormListener | SubmitFormListener): () => void {
135
+ this._listeners[event] ??= [];
136
+
137
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
+ this._listeners[event]?.push(listener as any);
139
+
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
+ return () => this.off(event as any, listener);
142
+ }
143
+
144
+ public off(event: 'focus', listener: FocusFormListener): void;
145
+ public off(event: 'submit', listener: SubmitFormListener): void;
146
+ public off(event: 'focus' | 'submit', listener: FocusFormListener | SubmitFormListener): void {
147
+ arrayRemove(this._listeners[event] ?? [], listener);
148
+ }
149
+
150
+ public async focus(input: string): Promise<void> {
151
+ await nextTick();
152
+
153
+ this._listeners['focus']?.forEach((listener) => listener(input));
114
154
  }
115
155
 
116
156
  protected __get(property: string): unknown {
@@ -1,3 +1,4 @@
1
1
  export * from './Form';
2
2
  export * from './composition';
3
3
  export * from './utils';
4
+ export { default as Form } from './Form';
@@ -8,6 +8,13 @@ export function booleanInput(defaultValue?: boolean): FormFieldDefinition<typeof
8
8
  };
9
9
  }
10
10
 
11
+ export function dateInput(defaultValue?: Date): FormFieldDefinition<typeof FormFieldTypes.Date> {
12
+ return {
13
+ default: defaultValue,
14
+ type: FormFieldTypes.Date,
15
+ };
16
+ }
17
+
11
18
  export function requiredBooleanInput(
12
19
  defaultValue?: boolean,
13
20
  ): FormFieldDefinition<typeof FormFieldTypes.Boolean, 'required'> {
@@ -18,6 +25,14 @@ export function requiredBooleanInput(
18
25
  };
19
26
  }
20
27
 
28
+ export function requiredDateInput(defaultValue?: Date): FormFieldDefinition<typeof FormFieldTypes.Date> {
29
+ return {
30
+ default: defaultValue,
31
+ type: FormFieldTypes.Date,
32
+ rules: 'required',
33
+ };
34
+ }
35
+
21
36
  export function requiredNumberInput(
22
37
  defaultValue?: number,
23
38
  ): FormFieldDefinition<typeof FormFieldTypes.Number, 'required'> {
@@ -0,0 +1,5 @@
1
+ export default abstract class Job {
2
+
3
+ public abstract run(): Promise<void>;
4
+
5
+ }
@@ -0,0 +1,7 @@
1
+ import Job from './Job';
2
+
3
+ export { Job };
4
+
5
+ export async function dispatch(job: Job): Promise<void> {
6
+ await job.run();
7
+ }
package/src/lang/Lang.ts CHANGED
@@ -1,10 +1,11 @@
1
- import { facade, toString } from '@noeldemartin/utils';
1
+ import { facade } from '@noeldemartin/utils';
2
2
 
3
3
  import App from '@/services/App';
4
4
  import Service from '@/services/Service';
5
5
 
6
6
  export interface LangProvider {
7
7
  translate(key: string, parameters?: Record<string, unknown>): string;
8
+ translateWithDefault(key: string, defaultMessage: string, parameters?: Record<string, unknown>): string;
8
9
  }
9
10
 
10
11
  export class LangService extends Service {
@@ -21,6 +22,12 @@ export class LangService extends Service {
21
22
 
22
23
  return key;
23
24
  },
25
+ translateWithDefault: (_, defaultMessage) => {
26
+ // eslint-disable-next-line no-console
27
+ App.development && console.warn('Lang provider is missing');
28
+
29
+ return defaultMessage;
30
+ },
24
31
  };
25
32
  }
26
33
 
@@ -32,29 +39,10 @@ export class LangService extends Service {
32
39
  return this.provider.translate(key, parameters) ?? key;
33
40
  }
34
41
 
35
- public translateWithDefault(key: string, defaultMessage: string): string;
36
- public translateWithDefault(key: string, parameters: Record<string, unknown>, defaultMessage: string): string;
37
- public translateWithDefault(
38
- key: string,
39
- defaultMessageOrParameters?: string | Record<string, unknown>,
40
- defaultMessage?: string,
41
- ): string {
42
- defaultMessage ??= defaultMessageOrParameters as string;
43
-
44
- const parameters = typeof defaultMessageOrParameters === 'string' ? {} : defaultMessageOrParameters ?? {};
45
- const message = this.provider.translate(key, parameters) ?? key;
46
-
47
- if (message === key) {
48
- return Object.entries(parameters).reduce(
49
- (renderedMessage, [name, value]) =>
50
- renderedMessage.replace(new RegExp(`\\{\\s*${name}\\s*\\}`, 'g'), toString(value)),
51
- defaultMessage,
52
- );
53
- }
54
-
55
- return message;
42
+ public translateWithDefault(key: string, defaultMessage: string, parameters: Record<string, unknown> = {}): string {
43
+ return this.provider.translateWithDefault(key, defaultMessage, parameters);
56
44
  }
57
45
 
58
46
  }
59
47
 
60
- export default facade(new LangService());
48
+ export default facade(LangService);
package/src/main.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  export * from './bootstrap';
2
2
  export * from './components';
3
+ export * from './directives';
3
4
  export * from './errors';
4
5
  export * from './forms';
6
+ export * from './jobs';
5
7
  export * from './lang';
6
8
  export * from './plugins';
7
9
  export * from './services';
10
+ export * from './testing';
8
11
  export * from './ui';
9
12
  export * from './utils';
@@ -9,10 +9,9 @@ export default defineServiceState({
9
9
  plugins: {} as Record<string, Plugin>,
10
10
  environment: Aerogel.environment,
11
11
  sourceUrl: Aerogel.sourceUrl,
12
- isMounted: false,
13
12
  },
14
13
  computed: {
15
14
  development: (state) => state.environment === 'development',
16
- testing: (state) => state.environment === 'testing',
15
+ testing: (state) => state.environment === 'test' || state.environment === 'testing',
17
16
  },
18
17
  });
@@ -1,4 +1,4 @@
1
- import { facade, forever, updateLocationQueryParameters } from '@noeldemartin/utils';
1
+ import { PromisedValue, facade, forever, updateLocationQueryParameters } from '@noeldemartin/utils';
2
2
 
3
3
  import Events from '@/services/Events';
4
4
  import type { Plugin } from '@/plugins';
@@ -7,6 +7,23 @@ import Service from './App.state';
7
7
 
8
8
  export class AppService extends Service {
9
9
 
10
+ public readonly ready = new PromisedValue<void>();
11
+ public readonly mounted = new PromisedValue<void>();
12
+
13
+ public isReady(): boolean {
14
+ return this.ready.isResolved();
15
+ }
16
+
17
+ public isMounted(): boolean {
18
+ return this.mounted.isResolved();
19
+ }
20
+
21
+ public async whenReady<T>(callback: () => T): Promise<T> {
22
+ const result = await this.ready.then(callback);
23
+
24
+ return result;
25
+ }
26
+
10
27
  public async reload(queryParameters?: Record<string, string | undefined>): Promise<void> {
11
28
  queryParameters && updateLocationQueryParameters(queryParameters);
12
29
 
@@ -21,9 +38,10 @@ export class AppService extends Service {
21
38
  }
22
39
 
23
40
  protected async boot(): Promise<void> {
24
- Events.once('application-mounted', () => this.setState({ isMounted: true }));
41
+ Events.once('application-ready', () => this.ready.resolve());
42
+ Events.once('application-mounted', () => this.mounted.resolve());
25
43
  }
26
44
 
27
45
  }
28
46
 
29
- export default facade(new AppService());
47
+ export default facade(AppService);