@aerogel/core 0.0.0-next.bde642c4a8096c5fc3d5e676c2115da23f4bf1d8 → 0.0.0-next.c4825c5cbe0fe3257e478c2a7ec8df27d5a72305

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 (60) 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 +371 -69
  4. package/dist/aerogel-core.esm.js +1 -1
  5. package/dist/aerogel-core.esm.js.map +1 -1
  6. package/package.json +3 -3
  7. package/src/bootstrap/index.ts +12 -2
  8. package/src/components/headless/forms/AGHeadlessInput.ts +1 -0
  9. package/src/components/headless/forms/AGHeadlessInput.vue +7 -0
  10. package/src/components/headless/forms/AGHeadlessInputInput.vue +1 -0
  11. package/src/components/headless/forms/AGHeadlessInputTextArea.vue +1 -0
  12. package/src/components/headless/modals/AGHeadlessModal.ts +3 -1
  13. package/src/components/headless/modals/AGHeadlessModal.vue +10 -4
  14. package/src/components/headless/modals/AGHeadlessModalPanel.vue +10 -6
  15. package/src/components/headless/modals/AGHeadlessModalTitle.vue +14 -4
  16. package/src/components/lib/AGMarkdown.vue +14 -1
  17. package/src/components/lib/AGMeasured.vue +1 -0
  18. package/src/components/lib/AGProgressBar.vue +30 -0
  19. package/src/components/lib/index.ts +1 -0
  20. package/src/components/modals/AGAlertModal.ts +5 -2
  21. package/src/components/modals/AGConfirmModal.ts +14 -5
  22. package/src/components/modals/AGConfirmModal.vue +2 -2
  23. package/src/components/modals/AGErrorReportModal.ts +5 -2
  24. package/src/components/modals/AGLoadingModal.ts +10 -4
  25. package/src/components/modals/AGModal.ts +1 -0
  26. package/src/components/modals/AGModalContext.vue +14 -4
  27. package/src/components/modals/AGPromptModal.ts +9 -4
  28. package/src/directives/measure.ts +24 -5
  29. package/src/errors/JobCancelledError.ts +3 -0
  30. package/src/errors/utils.ts +16 -0
  31. package/src/forms/Form.test.ts +28 -0
  32. package/src/forms/Form.ts +24 -6
  33. package/src/forms/index.ts +2 -1
  34. package/src/forms/utils.ts +20 -4
  35. package/src/forms/validation.ts +19 -0
  36. package/src/jobs/Job.ts +144 -2
  37. package/src/jobs/index.ts +4 -1
  38. package/src/jobs/listeners.ts +3 -0
  39. package/src/jobs/status.ts +4 -0
  40. package/src/lang/Lang.ts +4 -0
  41. package/src/services/App.state.ts +9 -1
  42. package/src/services/App.ts +5 -0
  43. package/src/services/Events.ts +13 -3
  44. package/src/services/Service.ts +107 -44
  45. package/src/services/Storage.ts +20 -0
  46. package/src/services/index.ts +7 -2
  47. package/src/services/utils.ts +18 -0
  48. package/src/testing/setup.ts +11 -3
  49. package/src/ui/UI.state.ts +7 -0
  50. package/src/ui/UI.ts +136 -43
  51. package/src/ui/index.ts +1 -0
  52. package/src/ui/utils.ts +16 -0
  53. package/src/utils/composition/persistent.test.ts +33 -0
  54. package/src/utils/composition/persistent.ts +11 -0
  55. package/src/utils/composition/state.test.ts +47 -0
  56. package/src/utils/composition/state.ts +24 -0
  57. package/src/utils/index.ts +1 -0
  58. package/src/utils/markdown.test.ts +50 -0
  59. package/src/utils/markdown.ts +17 -2
  60. package/src/utils/vue.ts +11 -1
@@ -1,37 +1,56 @@
1
- import { MagicObject, PromisedValue, Storage, fail, isEmpty, objectDeepClone, objectOnly } from '@noeldemartin/utils';
2
- import type { Constructor } from '@noeldemartin/utils';
3
- import type { MaybeRef } from 'vue';
1
+ import {
2
+ MagicObject,
3
+ PromisedValue,
4
+ Storage,
5
+ arrayFrom,
6
+ fail,
7
+ isEmpty,
8
+ objectDeepClone,
9
+ objectOnly,
10
+ } from '@noeldemartin/utils';
11
+ import type { Constructor, Nullable } from '@noeldemartin/utils';
4
12
  import type { Store } from 'pinia';
5
13
 
6
14
  import ServiceBootError from '@/errors/ServiceBootError';
7
15
  import { defineServiceStore } from '@/services/store';
16
+ import type { Unref } from '@/utils/vue';
8
17
 
9
18
  export type ServiceState = Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
10
19
  export type DefaultServiceState = any; // eslint-disable-line @typescript-eslint/no-explicit-any
11
20
  export type ServiceConstructor<T extends Service = Service> = Constructor<T> & typeof Service;
12
- export type UnrefServiceState<State extends ServiceState> = {
13
- [K in keyof State]: State[K] extends MaybeRef<infer T> ? T : State[K];
14
- };
15
21
 
16
22
  export type ComputedStateDefinition<TState extends ServiceState, TComputedState extends ServiceState> = {
17
- [K in keyof TComputedState]: (state: UnrefServiceState<TState>) => TComputedState[K];
23
+ [K in keyof TComputedState]: (state: Unref<TState>) => TComputedState[K];
18
24
  } & ThisType<{
19
25
  readonly [K in keyof TComputedState]: TComputedState[K];
20
26
  }>;
21
27
 
28
+ export type StateWatchers<TService extends Service, TState extends ServiceState> = {
29
+ [K in keyof TState]?: (this: TService, value: TState[K], oldValue: TState[K]) => unknown;
30
+ };
31
+
32
+ export type ServiceWithState<
33
+ State extends ServiceState = ServiceState,
34
+ ComputedState extends ServiceState = {},
35
+ ServiceStorage = Partial<State>
36
+ > = Constructor<Unref<State>> &
37
+ Constructor<ComputedState> &
38
+ Constructor<Service<Unref<State>, ComputedState, Unref<ServiceStorage>>>;
39
+
22
40
  export function defineServiceState<
23
41
  State extends ServiceState = ServiceState,
24
- ComputedState extends ServiceState = {}
42
+ ComputedState extends ServiceState = {},
43
+ ServiceStorage = Partial<State>
25
44
  >(options: {
26
45
  name: string;
27
46
  initialState: State | (() => State);
28
47
  persist?: (keyof State)[];
48
+ watch?: StateWatchers<Service, State>;
29
49
  computed?: ComputedStateDefinition<State, ComputedState>;
30
- serialize?: (state: Partial<State>) => Partial<State>;
31
- }): Constructor<UnrefServiceState<State>> &
32
- Constructor<ComputedState> &
33
- Constructor<Service<UnrefServiceState<State>, ComputedState, Partial<UnrefServiceState<State>>>> {
34
- return class extends Service<UnrefServiceState<State>, ComputedState> {
50
+ serialize?: (state: Partial<State>) => ServiceStorage;
51
+ restore?: (state: ServiceStorage) => Partial<State>;
52
+ }): ServiceWithState<State, ComputedState, ServiceStorage> {
53
+ return class extends Service<Unref<State>, ComputedState, ServiceStorage> {
35
54
 
36
55
  public static persist = (options.persist as string[]) ?? [];
37
56
 
@@ -43,7 +62,7 @@ export function defineServiceState<
43
62
  return options.name ?? null;
44
63
  }
45
64
 
46
- protected getInitialState(): UnrefServiceState<State> {
65
+ protected getInitialState(): Unref<State> {
47
66
  if (typeof options.initialState === 'function') {
48
67
  return options.initialState();
49
68
  }
@@ -64,26 +83,32 @@ export function defineServiceState<
64
83
  state[key as keyof State] = value;
65
84
 
66
85
  return state;
67
- }, {} as UnrefServiceState<State>);
86
+ }, {} as Unref<State>);
68
87
  }
69
88
 
70
- protected getComputedStateDefinition(): ComputedStateDefinition<UnrefServiceState<State>, ComputedState> {
71
- return (options.computed ?? {}) as ComputedStateDefinition<UnrefServiceState<State>, ComputedState>;
89
+ protected getComputedStateDefinition(): ComputedStateDefinition<Unref<State>, ComputedState> {
90
+ return (options.computed ?? {}) as ComputedStateDefinition<Unref<State>, ComputedState>;
72
91
  }
73
92
 
74
- protected serializePersistedState(state: Partial<State>): Partial<State> {
75
- return options.serialize?.(state) ?? state;
93
+ protected getStateWatchers(): StateWatchers<Service, Unref<State>> {
94
+ return (options.watch ?? {}) as StateWatchers<Service, Unref<State>>;
76
95
  }
77
96
 
78
- } as unknown as Constructor<UnrefServiceState<State>> &
79
- Constructor<ComputedState> &
80
- Constructor<Service<UnrefServiceState<State>, ComputedState, Partial<UnrefServiceState<State>>>>;
97
+ protected serializePersistedState(state: Partial<State>): ServiceStorage {
98
+ return options.serialize?.(state) ?? (state as ServiceStorage);
99
+ }
100
+
101
+ protected deserializePersistedState(state: ServiceStorage): Partial<State> {
102
+ return options.restore?.(state) ?? (state as Partial<State>);
103
+ }
104
+
105
+ } as unknown as ServiceWithState<State, ComputedState, ServiceStorage>;
81
106
  }
82
107
 
83
108
  export default class Service<
84
109
  State extends ServiceState = DefaultServiceState,
85
110
  ComputedState extends ServiceState = {},
86
- ServiceStorage extends Partial<State> = Partial<State>
111
+ ServiceStorage = Partial<State>
87
112
  > extends MagicObject {
88
113
 
89
114
  public static persist: string[] = [];
@@ -91,6 +116,7 @@ export default class Service<
91
116
  protected _name: string;
92
117
  private _booted: PromisedValue<void>;
93
118
  private _computedStateKeys: Set<keyof State>;
119
+ private _watchers: StateWatchers<Service, State>;
94
120
  private _store: Store<string, State, ComputedState, {}> | false;
95
121
 
96
122
  constructor() {
@@ -101,6 +127,7 @@ export default class Service<
101
127
  this._name = this.getName() ?? new.target.name;
102
128
  this._booted = new PromisedValue();
103
129
  this._computedStateKeys = new Set(Object.keys(getters));
130
+ this._watchers = this.getStateWatchers();
104
131
  this._store =
105
132
  this.usesStore() &&
106
133
  defineServiceStore(this._name, {
@@ -115,6 +142,12 @@ export default class Service<
115
142
  return this._booted;
116
143
  }
117
144
 
145
+ public static<T extends typeof Service>(): T;
146
+ public static<T extends typeof Service, K extends keyof T>(property: K): T[K];
147
+ public static<T extends typeof Service, K extends keyof T>(property?: K): T | T[K] {
148
+ return super.static<T, K>(property as K);
149
+ }
150
+
118
151
  public launch(): Promise<void> {
119
152
  const handleError = (error: unknown) => this._booted.reject(new ServiceBootError(this._name, error));
120
153
 
@@ -162,13 +195,28 @@ export default class Service<
162
195
  return;
163
196
  }
164
197
 
165
- const state = (
166
- typeof stateOrProperty === 'string' ? { [stateOrProperty]: value } : stateOrProperty
167
- ) as Partial<State>;
198
+ const update = typeof stateOrProperty === 'string' ? { [stateOrProperty]: value } : stateOrProperty;
199
+ const old = objectOnly(this._store.$state as State, Object.keys(update));
168
200
 
169
- Object.assign(this._store.$state, state);
201
+ Object.assign(this._store.$state, update);
202
+ this.onStateUpdated(update as Partial<State>, old as Partial<State>);
203
+ }
170
204
 
171
- this.onStateUpdated(state);
205
+ public updatePersistedState<T extends keyof State>(key: T): void;
206
+ public updatePersistedState<T extends keyof State>(keys: T[]): void;
207
+ public updatePersistedState<T extends keyof State>(keyOrKeys: T | T[]): void {
208
+ if (!this._store) {
209
+ return;
210
+ }
211
+
212
+ const keys = arrayFrom(keyOrKeys) as Array<keyof State>;
213
+ const state = objectOnly(this._store.$state as State, keys);
214
+
215
+ if (isEmpty(state)) {
216
+ return;
217
+ }
218
+
219
+ this.onPersistentStateUpdated(state);
172
220
  }
173
221
 
174
222
  protected __get(property: string): unknown {
@@ -183,15 +231,25 @@ export default class Service<
183
231
  this.setState({ [property]: value } as Partial<State>);
184
232
  }
185
233
 
186
- protected onStateUpdated(state: Partial<State>): void {
187
- // TODO fix this.static()
188
- const persist = (this.constructor as unknown as { persist: string[] }).persist;
189
- const persisted = objectOnly(state, persist);
234
+ protected onStateUpdated(update: Partial<State>, old: Partial<State>): void {
235
+ const persisted = objectOnly(update, this.static('persist'));
190
236
 
191
- if (isEmpty(persisted)) {
192
- return;
237
+ if (!isEmpty(persisted)) {
238
+ this.onPersistentStateUpdated(persisted as Partial<State>);
193
239
  }
194
240
 
241
+ for (const property in update) {
242
+ const watcher = this._watchers[property] as Nullable<(value: unknown, oldValue: unknown) => unknown>;
243
+
244
+ if (!watcher || update[property] === old[property]) {
245
+ continue;
246
+ }
247
+
248
+ watcher.call(this, update[property], old[property]);
249
+ }
250
+ }
251
+
252
+ protected onPersistentStateUpdated(persisted: Partial<State>): void {
195
253
  const storage = Storage.get<ServiceStorage>(this._name);
196
254
 
197
255
  if (!storage) {
@@ -220,34 +278,39 @@ export default class Service<
220
278
  return {} as ComputedStateDefinition<State, ComputedState>;
221
279
  }
222
280
 
223
- protected serializePersistedState(state: Partial<State>): Partial<State> {
224
- return state;
281
+ protected getStateWatchers(): StateWatchers<Service, State> {
282
+ return {};
283
+ }
284
+
285
+ protected serializePersistedState(state: Partial<State>): ServiceStorage {
286
+ return state as ServiceStorage;
287
+ }
288
+
289
+ protected deserializePersistedState(state: ServiceStorage): Partial<State> {
290
+ return state as Partial<State>;
225
291
  }
226
292
 
227
293
  protected async frameworkBoot(): Promise<void> {
228
- this.initializePersistedState();
294
+ this.restorePersistedState();
229
295
  }
230
296
 
231
297
  protected async boot(): Promise<void> {
232
298
  // Placeholder for overrides, don't place any functionality here.
233
299
  }
234
300
 
235
- protected initializePersistedState(): void {
236
- // TODO fix this.static()
237
- const persist = (this.constructor as unknown as { persist: string[] }).persist;
238
-
239
- if (!this.usesStore() || isEmpty(persist)) {
301
+ protected restorePersistedState(): void {
302
+ if (!this.usesStore() || isEmpty(this.static('persist'))) {
240
303
  return;
241
304
  }
242
305
 
243
306
  if (Storage.has(this._name)) {
244
307
  const persisted = Storage.require<ServiceStorage>(this._name);
245
- this.setState(persisted);
308
+ this.setState(this.deserializePersistedState(persisted));
246
309
 
247
310
  return;
248
311
  }
249
312
 
250
- Storage.set(this._name, objectOnly(this.getState(), persist));
313
+ Storage.set(this._name, objectOnly(this.getState(), this.static('persist')));
251
314
  }
252
315
 
253
316
  protected requireStore(): Store<string, State, ComputedState, {}> {
@@ -0,0 +1,20 @@
1
+ import { facade } from '@noeldemartin/utils';
2
+
3
+ import Events from '@/services/Events';
4
+ import Service from '@/services/Service';
5
+
6
+ export class StorageService extends Service {
7
+
8
+ public async purge(): Promise<void> {
9
+ await Events.emit('purge-storage');
10
+ }
11
+
12
+ }
13
+
14
+ export default facade(StorageService);
15
+
16
+ declare module '@/services/Events' {
17
+ export interface EventsPayload {
18
+ 'purge-storage': void;
19
+ }
20
+ }
@@ -6,6 +6,7 @@ import App from './App';
6
6
  import Cache from './Cache';
7
7
  import Events from './Events';
8
8
  import Service from './Service';
9
+ import Storage from './Storage';
9
10
  import { getPiniaStore } from './store';
10
11
 
11
12
  export * from './App';
@@ -13,12 +14,14 @@ export * from './Cache';
13
14
  export * from './Events';
14
15
  export * from './Service';
15
16
  export * from './store';
17
+ export * from './utils';
16
18
 
17
- export { App, Cache, Events, Service };
19
+ export { App, Cache, Events, Storage, Service };
18
20
 
19
21
  const defaultServices = {
20
22
  $app: App,
21
23
  $events: Events,
24
+ $storage: Storage,
22
25
  };
23
26
 
24
27
  export type DefaultServices = typeof defaultServices;
@@ -36,7 +39,9 @@ export async function bootServices(app: VueApp, services: Record<string, Service
36
39
 
37
40
  Object.assign(app.config.globalProperties, services);
38
41
 
39
- App.development && Object.assign(window, services);
42
+ if (App.development || App.testing) {
43
+ Object.assign(globalThis, services);
44
+ }
40
45
  }
41
46
 
42
47
  export default definePlugin({
@@ -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
+ }
@@ -1,16 +1,24 @@
1
- import { mock, tap } from '@noeldemartin/utils';
1
+ import { mock, tap, toString } from '@noeldemartin/utils';
2
2
  import { beforeEach, vi } from 'vitest';
3
3
 
4
4
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
5
  tap(globalThis, (global: any) => {
6
+ const localStorage: Record<string, string> = {};
7
+
6
8
  global.jest = vi;
7
9
  global.navigator = { languages: ['en'] };
8
10
  global.localStorage = mock<Storage>({
9
- getItem: () => null,
10
- setItem: () => null,
11
+ getItem: (key) => localStorage[key] ?? null,
12
+ setItem(key, value) {
13
+ localStorage[key] = toString(value);
14
+ },
11
15
  });
12
16
  });
13
17
 
18
+ vi.mock('dompurify', async () => {
19
+ return { default: { sanitize: (html: string) => html } };
20
+ });
21
+
14
22
  beforeEach(() => {
15
23
  vi.stubGlobal('document', {
16
24
  querySelector: () => null,
@@ -2,6 +2,8 @@ import type { Component } from 'vue';
2
2
 
3
3
  import { defineServiceState } from '@/services/Service';
4
4
 
5
+ import { Layouts, getCurrentLayout } from './utils';
6
+
5
7
  export interface Modal<T = unknown> {
6
8
  id: string;
7
9
  properties: Record<string, unknown>;
@@ -28,5 +30,10 @@ export default defineServiceState({
28
30
  initialState: {
29
31
  modals: [] as Modal[],
30
32
  snackbars: [] as Snackbar[],
33
+ layout: getCurrentLayout(),
34
+ },
35
+ computed: {
36
+ mobile: ({ layout }) => layout === Layouts.Mobile,
37
+ desktop: ({ layout }) => layout === Layouts.Desktop,
31
38
  },
32
39
  });