Skip to content

Commit cd8dce2

Browse files
committed
Merge remote-tracking branch 'origin/main' into v14/bugfix/mntp-min-max-validation
2 parents 38af5c7 + daf177b commit cd8dce2

File tree

6 files changed

+134
-12
lines changed

6 files changed

+134
-12
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
2+
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
3+
import { OpenAPI } from '@umbraco-cms/backoffice/external/backend-api';
4+
import {
5+
extractUmbNotificationColor,
6+
isUmbNotifications,
7+
UMB_NOTIFICATION_CONTEXT,
8+
UMB_NOTIFICATION_HEADER,
9+
} from '@umbraco-cms/backoffice/notification';
10+
11+
/**
12+
* Controller that adds interceptors to the OpenAPI client
13+
*/
14+
export class UmbApiInterceptorController extends UmbControllerBase {
15+
constructor(host: UmbControllerHost) {
16+
super(host);
17+
this.#addUmbNotificationsInterceptor();
18+
}
19+
20+
/**
21+
* Interceptor which checks responses for the umb-notifications header and displays them as a notification if any. Removes the umb-notifications from the headers.
22+
*/
23+
#addUmbNotificationsInterceptor() {
24+
OpenAPI.interceptors.response.use((response) => {
25+
const umbNotifications = response.headers.get(UMB_NOTIFICATION_HEADER);
26+
if (!umbNotifications) return response;
27+
28+
const notifications = JSON.parse(umbNotifications);
29+
if (!isUmbNotifications(notifications)) return response;
30+
31+
this.getContext(UMB_NOTIFICATION_CONTEXT).then((notificationContext) => {
32+
for (const notification of notifications) {
33+
notificationContext.peek(extractUmbNotificationColor(notification.type), {
34+
data: { headline: notification.category, message: notification.message },
35+
});
36+
}
37+
});
38+
39+
const newHeader = new Headers();
40+
for (const header of response.headers.entries()) {
41+
const [key, value] = header;
42+
if (key !== UMB_NOTIFICATION_HEADER) newHeader.set(key, value);
43+
}
44+
45+
const newResponse = new Response(response.body, {
46+
headers: newHeader,
47+
status: response.status,
48+
statusText: response.statusText,
49+
});
50+
51+
return newResponse;
52+
});
53+
}
54+
}

src/apps/app/app.element.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { UmbAppErrorElement } from './app-error.element.js';
33
import { UmbAppContext } from './app.context.js';
44
import { UmbServerConnection } from './server-connection.js';
55
import { UmbAppAuthController } from './app-auth.controller.js';
6+
import { UmbApiInterceptorController } from './api-interceptor.controller.js';
67
import type { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
78
import { UmbAuthContext } from '@umbraco-cms/backoffice/auth';
89
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
@@ -147,6 +148,8 @@ export class UmbAppElement extends UmbLitElement {
147148

148149
OpenAPI.BASE = window.location.origin;
149150

151+
new UmbApiInterceptorController(this);
152+
150153
new UmbBundleExtensionInitializer(this, umbExtensionsRegistry);
151154

152155
new UUIIconRegistryEssential().attach(this);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { UmbNotificationColor } from './notification.context.js';
2+
import { EventMessageTypeModel } from '@umbraco-cms/backoffice/external/backend-api';
3+
4+
export function extractUmbNotificationColor(type: EventMessageTypeModel): UmbNotificationColor {
5+
switch (type) {
6+
case EventMessageTypeModel.ERROR:
7+
return 'danger';
8+
case EventMessageTypeModel.WARNING:
9+
return 'warning';
10+
case EventMessageTypeModel.INFO:
11+
case EventMessageTypeModel.DEFAULT:
12+
return 'default';
13+
case EventMessageTypeModel.SUCCESS:
14+
return 'positive';
15+
default:
16+
return '';
17+
}
18+
}

src/packages/core/notification/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ import './layouts/default/index.js';
22

33
export * from './notification.context.js';
44
export * from './notification-handler.js';
5+
6+
export * from './isUmbNotifications.function.js';
7+
export * from './extractUmbNotificationColor.function.js';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { EventMessageTypeModel } from '@umbraco-cms/backoffice/external/backend-api';
2+
3+
function objectIsUmbNotification(notification: unknown): notification is UmbNotificationsEventModel {
4+
if (typeof notification !== 'object' || notification === null) {
5+
return false;
6+
}
7+
const object = notification as UmbNotificationsEventModel;
8+
return (
9+
typeof object.category === 'string' &&
10+
typeof object.message === 'string' &&
11+
typeof object.type === 'string' &&
12+
Object.values(EventMessageTypeModel).includes(object.type)
13+
);
14+
}
15+
16+
export interface UmbNotificationsEventModel {
17+
category: string;
18+
message: string;
19+
type: EventMessageTypeModel;
20+
}
21+
22+
export function isUmbNotifications(notifications: Array<unknown>): notifications is Array<UmbNotificationsEventModel> {
23+
return notifications.every(objectIsUmbNotification);
24+
}
25+
26+
export const UMB_NOTIFICATION_HEADER = 'umb-notifications';

src/packages/core/resources/resource.controller.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
66
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
77
import { UMB_NOTIFICATION_CONTEXT, type UmbNotificationOptions } from '@umbraco-cms/backoffice/notification';
88
import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository';
9+
import type { ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api';
910

1011
export class UmbResourceController extends UmbControllerBase {
1112
#promise: Promise<any>;
@@ -57,8 +58,8 @@ export class UmbResourceController extends UmbControllerBase {
5758
* If the executor function throws an error, then show the details in a notification.
5859
*/
5960
async tryExecuteAndNotify<T>(options?: UmbNotificationOptions): Promise<UmbDataSourceResponse<T>> {
60-
const { data, error: _error } = await UmbResourceController.tryExecute<T>(this.#promise);
61-
const error: any = _error;
61+
const { data, error } = await UmbResourceController.tryExecute<T>(this.#promise);
62+
6263
if (error) {
6364
/**
6465
* Determine if we want to show a notification or just log the error to the console.
@@ -71,18 +72,33 @@ export class UmbResourceController extends UmbControllerBase {
7172
} else {
7273
console.group('ApiError caught in UmbResourceController');
7374
console.error('Request failed', error.request);
74-
console.error('ProblemDetails', error.body);
75+
console.error('Request body', error.body);
7576
console.error('Error', error);
7677

78+
let problemDetails: ProblemDetails | null = null;
79+
7780
// ApiError - body could hold a ProblemDetails from the server
7881
if (typeof error.body !== 'undefined' && !!error.body) {
7982
try {
80-
(error as any).body = typeof error.body === 'string' ? JSON.parse(error.body) : error.body;
83+
(error as any).body = problemDetails = typeof error.body === 'string' ? JSON.parse(error.body) : error.body;
8184
} catch (e) {
8285
console.error('Error parsing error body (expected JSON)', e);
8386
}
8487
}
8588

89+
/**
90+
* Check if the operation status ends with `ByNotification` and if so, don't show a notification
91+
* This is a special case where the operation was cancelled by the server and the client gets a notification header instead.
92+
*/
93+
let isCancelledByNotification = false;
94+
if (
95+
problemDetails?.operationStatus &&
96+
typeof problemDetails.operationStatus === 'string' &&
97+
problemDetails.operationStatus.endsWith('ByNotification')
98+
) {
99+
isCancelledByNotification = true;
100+
}
101+
86102
// Go through the error status codes and act accordingly
87103
switch (error.status ?? 0) {
88104
case 401: {
@@ -103,14 +119,14 @@ export class UmbResourceController extends UmbControllerBase {
103119
case 500:
104120
// Server Error
105121

106-
if (this.#notificationContext) {
107-
let headline = error.body?.title ?? error.name ?? 'Server Error';
122+
if (!isCancelledByNotification && this.#notificationContext) {
123+
let headline = problemDetails?.title ?? error.name ?? 'Server Error';
108124
let message = 'A fatal server error occurred. If this continues, please reach out to your administrator.';
109125

110126
// Special handling for ObjectCacheAppCache corruption errors, which we are investigating
111127
if (
112-
error.body?.detail?.includes('ObjectCacheAppCache') ||
113-
error.body?.detail?.includes('Umbraco.Cms.Infrastructure.Scoping.Scope.DisposeLastScope()')
128+
problemDetails?.detail?.includes('ObjectCacheAppCache') ||
129+
problemDetails?.detail?.includes('Umbraco.Cms.Infrastructure.Scoping.Scope.DisposeLastScope()')
114130
) {
115131
headline = 'Please restart the server';
116132
message =
@@ -128,12 +144,14 @@ export class UmbResourceController extends UmbControllerBase {
128144
break;
129145
default:
130146
// Other errors
131-
if (this.#notificationContext) {
147+
if (!isCancelledByNotification && this.#notificationContext) {
132148
this.#notificationContext.peek('danger', {
133149
data: {
134-
headline: error.body?.title ?? error.name ?? 'Server Error',
135-
message: error.body?.detail ?? error.message ?? 'Something went wrong',
136-
structuredList: error.body.errors,
150+
headline: problemDetails?.title ?? error.name ?? 'Server Error',
151+
message: problemDetails?.detail ?? error.message ?? 'Something went wrong',
152+
structuredList: problemDetails?.errors
153+
? (problemDetails.errors as Record<string, Array<unknown>>)
154+
: undefined,
137155
},
138156
...options,
139157
});

0 commit comments

Comments
 (0)