Skip to content

Commit 698ea0c

Browse files
feat(admin-ui): Expose registerAlert provider for custom UI alerts
Relates to #2503
1 parent 7089a62 commit 698ea0c

File tree

5 files changed

+163
-31
lines changed

5 files changed

+163
-31
lines changed

packages/admin-ui/src/lib/core/src/core.module.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { BrowserModule, Title } from '@angular/platform-browser';
55
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
66
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
77
import { TranslateCompiler, TranslateLoader, TranslateModule } from '@ngx-translate/core';
8+
import { interval } from 'rxjs';
89

910
import { getAppConfig } from './app.config';
1011
import { getDefaultUiLanguage, getDefaultUiLocale } from './common/utilities/get-default-ui-language';
@@ -21,16 +22,14 @@ import { ThemeSwitcherComponent } from './components/theme-switcher/theme-switch
2122
import { UiLanguageSwitcherDialogComponent } from './components/ui-language-switcher-dialog/ui-language-switcher-dialog.component';
2223
import { UserMenuComponent } from './components/user-menu/user-menu.component';
2324
import { DataModule } from './data/data.module';
24-
import { DataService } from './data/providers/data.service';
2525
import { AlertsService } from './providers/alerts/alerts.service';
2626
import { CustomHttpTranslationLoader } from './providers/i18n/custom-http-loader';
2727
import { InjectableTranslateMessageFormatCompiler } from './providers/i18n/custom-message-format-compiler';
2828
import { I18nService } from './providers/i18n/i18n.service';
2929
import { LocalStorageService } from './providers/local-storage/local-storage.service';
30-
import { NotificationService } from './providers/notification/notification.service';
30+
import { Permission } from './public_api';
3131
import { registerDefaultFormInputs } from './shared/dynamic-form-inputs/default-form-inputs';
3232
import { SharedModule } from './shared/shared.module';
33-
import { Permission } from './public_api';
3433

3534
@NgModule({
3635
imports: [
@@ -71,8 +70,6 @@ export class CoreModule {
7170
private localStorageService: LocalStorageService,
7271
private titleService: Title,
7372
private alertsService: AlertsService,
74-
private dataService: DataService,
75-
private notificationService: NotificationService,
7673
) {
7774
this.initUiLanguagesAndLocales();
7875
this.initUiTitle();
@@ -121,18 +118,19 @@ export class CoreModule {
121118
}
122119

123120
private initAlerts() {
121+
const pendingUpdatesId = 'pending-search-index-updates';
124122
this.alertsService.configureAlert({
125-
id: 'pending-search-index-updates',
123+
id: pendingUpdatesId,
126124
requiredPermissions: [Permission.ReadCatalog, Permission.ReadProduct],
127-
check: () =>
128-
this.dataService.product
125+
check: context =>
126+
context.dataService.product
129127
.getPendingSearchIndexUpdates()
130128
.mapSingle(({ pendingSearchIndexUpdates }) => pendingSearchIndexUpdates),
131-
recheckIntervalMs: 1000 * 30,
129+
recheck: () => interval(1000 * 30),
132130
isAlert: data => 0 < data,
133-
action: data => {
134-
this.dataService.product.runPendingSearchIndexUpdates().subscribe(value => {
135-
this.notificationService.info(_('catalog.running-search-index-updates'), {
131+
action: (data, context) => {
132+
context.dataService.product.runPendingSearchIndexUpdates().subscribe(() => {
133+
context.notificationService.info(_('catalog.running-search-index-updates'), {
136134
count: data,
137135
});
138136
});
@@ -142,7 +140,7 @@ export class CoreModule {
142140
translationVars: { count: data },
143141
}),
144142
});
145-
this.alertsService.refresh();
143+
this.alertsService.refresh(pendingUpdatesId);
146144
}
147145
}
148146

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { APP_INITIALIZER, FactoryProvider } from '@angular/core';
2+
import { AlertConfig, AlertsService } from '../providers/alerts/alerts.service';
3+
4+
/**
5+
* @description
6+
* Registers an alert which can be displayed in the Admin UI alert dropdown in the top bar.
7+
* The alert is configured using the {@link AlertConfig} object.
8+
*
9+
* @since 2.2.0
10+
* @docsCategory alerts
11+
*/
12+
export function registerAlert(config: AlertConfig): FactoryProvider {
13+
return {
14+
provide: APP_INITIALIZER,
15+
multi: true,
16+
useFactory: (alertsService: AlertsService) => () => {
17+
alertsService.configureAlert(config);
18+
alertsService.refresh(config.id);
19+
},
20+
deps: [AlertsService],
21+
};
22+
}

packages/admin-ui/src/lib/core/src/providers/alerts/alerts.service.ts

Lines changed: 127 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { Injectable } from '@angular/core';
1+
import { Injectable, Injector } from '@angular/core';
22
import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
33
import {
44
BehaviorSubject,
55
combineLatest,
66
first,
7-
interval,
87
isObservable,
98
Observable,
109
of,
@@ -13,15 +12,109 @@ import {
1312
} from 'rxjs';
1413
import { filter, map, startWith, take } from 'rxjs/operators';
1514
import { Permission } from '../../common/generated-types';
15+
import { DataService } from '../../data/providers/data.service';
16+
import { ModalService } from '../modal/modal.service';
17+
import { NotificationService } from '../notification/notification.service';
1618
import { PermissionsService } from '../permissions/permissions.service';
1719

20+
/**
21+
* @description
22+
* The context object which is passed to the `check`, `isAlert` and `action` functions of an
23+
* {@link AlertConfig} object.
24+
*/
25+
export interface AlertContext {
26+
/**
27+
* @description
28+
* The Angular [Injector](https://angular.dev/api/core/Injector) which can be used to get instances
29+
* of services and other providers available in the application.
30+
*/
31+
injector: Injector;
32+
/**
33+
* @description
34+
* The [DataService](/reference/admin-ui-api/services/data-service), which provides methods for querying the
35+
* server-side data.
36+
*/
37+
dataService: DataService;
38+
/**
39+
* @description
40+
* The [NotificationService](/reference/admin-ui-api/services/notification-service), which provides methods for
41+
* displaying notifications to the user.
42+
*/
43+
notificationService: NotificationService;
44+
/**
45+
* @description
46+
* The [ModalService](/reference/admin-ui-api/services/modal-service), which provides methods for
47+
* opening modal dialogs.
48+
*/
49+
modalService: ModalService;
50+
}
51+
52+
/**
53+
* @description
54+
* A configuration object for an Admin UI alert.
55+
*
56+
* @since 2.2.0
57+
* @docsCategory alerts
58+
*/
1859
export interface AlertConfig<T = any> {
60+
/**
61+
* @description
62+
* A unique identifier for the alert.
63+
*/
1964
id: string;
20-
check: () => T | Promise<T> | Observable<T>;
21-
recheckIntervalMs?: number;
22-
isAlert: (value: T) => boolean;
23-
action: (data: T) => void;
24-
label: (data: T) => { text: string; translationVars?: { [key: string]: string | number } };
65+
/**
66+
* @description
67+
* A function which is gets the data used to determine whether the alert should be shown.
68+
* Typically, this function will query the server or some other remote data source.
69+
*
70+
* This function will be called once when the Admin UI app bootstraps, and can be also
71+
* set to run at regular intervals by setting the `recheckIntervalMs` property.
72+
*/
73+
check: (context: AlertContext) => T | Promise<T> | Observable<T>;
74+
/**
75+
* @description
76+
* A function which returns an Observable which is used to determine when to re-run the `check`
77+
* function. Whenever the observable emits, the `check` function will be called again.
78+
*
79+
* A basic time-interval-based recheck can be achieved by using the `interval` function from RxJS.
80+
*
81+
* @example
82+
* ```ts
83+
* import { interval } from 'rxjs';
84+
*
85+
* // ...
86+
* recheck: () => interval(60_000)
87+
* ```
88+
*
89+
* If this is not set, the `check` function will only be called once when the Admin UI app bootstraps.
90+
*
91+
* @default undefined
92+
*/
93+
recheck?: (context: AlertContext) => Observable<any>;
94+
/**
95+
* @description
96+
* A function which determines whether the alert should be shown based on the data returned by the `check`
97+
* function.
98+
*/
99+
isAlert: (data: T, context: AlertContext) => boolean;
100+
/**
101+
* @description
102+
* A function which is called when the alert is clicked in the Admin UI.
103+
*/
104+
action: (data: T, context: AlertContext) => void;
105+
/**
106+
* @description
107+
* A function which returns the text used in the UI to describe the alert.
108+
*/
109+
label: (
110+
data: T,
111+
context: AlertContext,
112+
) => { text: string; translationVars?: { [key: string]: string | number } };
113+
/**
114+
* @description
115+
* A list of permissions which the current Administrator must have in order. If the current
116+
* Administrator does not have these permissions, none of the other alert functions will be called.
117+
*/
25118
requiredPermissions?: Permission[];
26119
}
27120

@@ -36,29 +129,32 @@ export class Alert<T> {
36129
activeAlert$: Observable<ActiveAlert | undefined>;
37130
private hasRun$ = new BehaviorSubject(false);
38131
private data$ = new BehaviorSubject<T | undefined>(undefined);
39-
constructor(private config: AlertConfig<T>) {
40-
if (this.config.recheckIntervalMs) {
41-
interval(this.config.recheckIntervalMs).subscribe(() => this.runCheck());
132+
constructor(
133+
private config: AlertConfig<T>,
134+
private context: AlertContext,
135+
) {
136+
if (this.config.recheck) {
137+
this.config.recheck(this.context).subscribe(() => this.runCheck());
42138
}
43139
this.activeAlert$ = combineLatest(this.data$, this.hasRun$).pipe(
44140
map(([data, hasRun]) => {
45141
if (!data) {
46142
return;
47143
}
48-
const isAlert = this.config.isAlert(data);
144+
const isAlert = this.config.isAlert(data, this.context);
49145
if (!isAlert) {
50146
return;
51147
}
52148
return {
53149
id: this.config.id,
54150
runAction: () => {
55151
if (!hasRun) {
56-
this.config.action(data);
152+
this.config.action(data, this.context);
57153
this.hasRun$.next(true);
58154
}
59155
},
60156
hasRun,
61-
label: this.config.label(data),
157+
label: this.config.label(data, this.context),
62158
};
63159
}),
64160
);
@@ -67,7 +163,7 @@ export class Alert<T> {
67163
return this.config.id;
68164
}
69165
runCheck() {
70-
const result = this.config.check();
166+
const result = this.config.check(this.context);
71167
if (result instanceof Promise) {
72168
result.then(data => this.data$.next(data));
73169
} else if (isObservable(result)) {
@@ -87,7 +183,13 @@ export class AlertsService {
87183
private alertsMap = new Map<string, Alert<any>>();
88184
private configUpdated = new Subject<void>();
89185

90-
constructor(private permissionsService: PermissionsService) {
186+
constructor(
187+
private permissionsService: PermissionsService,
188+
private injector: Injector,
189+
private dataService: DataService,
190+
private notificationService: NotificationService,
191+
private modalService: ModalService,
192+
) {
91193
const alerts$ = this.configUpdated.pipe(
92194
map(() => [...this.alertsMap.values()]),
93195
startWith([...this.alertsMap.values()]),
@@ -108,7 +210,7 @@ export class AlertsService {
108210
.pipe(first())
109211
.subscribe(hasPermissions => {
110212
if (hasPermissions) {
111-
this.alertsMap.set(config.id, new Alert(config));
213+
this.alertsMap.set(config.id, new Alert(config, this.createContext()));
112214
this.configUpdated.next();
113215
}
114216
});
@@ -131,4 +233,13 @@ export class AlertsService {
131233
this.alertsMap.forEach(config => config.runCheck());
132234
}
133235
}
236+
237+
protected createContext(): AlertContext {
238+
return {
239+
injector: this.injector,
240+
dataService: this.dataService,
241+
notificationService: this.notificationService,
242+
modalService: this.modalService,
243+
};
244+
}
134245
}

packages/admin-ui/src/lib/core/src/providers/modal/modal.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class ModalService {
3131
* displayed in the modal dialog. See example:
3232
*
3333
* @example
34-
* ```HTML
34+
* ```ts
3535
* class MyDialog implements Dialog {
3636
* resolveWith: (result?: any) => void;
3737
*
@@ -48,7 +48,7 @@ export class ModalService {
4848
* ```
4949
*
5050
* @example
51-
* ```HTML
51+
* ```html
5252
* <ng-template vdrDialogTitle>Title of the modal</ng-template>
5353
*
5454
* <p>

packages/admin-ui/src/lib/core/src/public_api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export * from './extension/add-nav-menu-item';
8181
export * from './extension/components/angular-route.component';
8282
export * from './extension/components/route.component';
8383
export * from './extension/providers/page-metadata.service';
84+
export * from './extension/register-alert';
8485
export * from './extension/register-bulk-action';
8586
export * from './extension/register-custom-detail-component';
8687
export * from './extension/register-dashboard-widget';

0 commit comments

Comments
 (0)