From ff36aeeac9c31e72ae06e60eb13f18b3fe04e187 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 15 Dec 2022 14:32:04 +0100 Subject: [PATCH] add resource mixin this mixin follows the Lit lifecycle and understands how to store and cancel running cancelable promises --- .../src/core/resource-api/resource.mixin.ts | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/core/resource-api/resource.mixin.ts diff --git a/src/Umbraco.Web.UI.Client/src/core/resource-api/resource.mixin.ts b/src/Umbraco.Web.UI.Client/src/core/resource-api/resource.mixin.ts new file mode 100644 index 0000000000..5f71d52bea --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/resource-api/resource.mixin.ts @@ -0,0 +1,107 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ApiError, CancelablePromise, ProblemDetails } from '@umbraco-cms/backend-api'; +import type { HTMLElementConstructor } from '@umbraco-cms/models'; +import { UmbNotificationOptions, UmbNotificationService, UmbNotificationDefaultData } from '@umbraco-cms/services'; +import { UmbContextConsumerMixin } from '@umbraco-cms/context-api'; + +export declare class UmbResourceMixinInterface { + tryExecute(promise: CancelablePromise): Promise<[T | undefined, ProblemDetails | undefined]>; + executeAndNotify(promise: CancelablePromise, options?: UmbNotificationOptions): Promise; + addResource(promise: CancelablePromise): void; + cancelAllResources(): void; +} + +export const UmbResourceMixin = (superClass: T) => { + class UmbResourceMixinClass extends UmbContextConsumerMixin(superClass) implements UmbResourceMixinInterface { + #promises: CancelablePromise[] = []; + + private _notificationService?: UmbNotificationService; + + connectedCallback() { + super.connectedCallback?.(); + this.#promises.length = 0; + this.consumeContext('umbNotificationService', (notificationService) => { + this._notificationService = notificationService; + }); + } + + disconnectedCallback() { + super.disconnectedCallback?.(); + this.cancelAllResources(); + } + + addResource(promise: CancelablePromise): void { + this.#promises.push(promise); + } + + /** + * Execute a given function and get the result as a promise. + */ + execute(func: CancelablePromise): Promise { + this.addResource(func); + return func; + } + + /** + * Wrap the {execute} function in a try/catch block and return a tuple with the result and the error. + */ + async tryExecute(func: CancelablePromise): Promise<[T | undefined, ProblemDetails | undefined]> { + try { + return [await this.execute(func), undefined]; + } catch (e) { + return [undefined, this.#toProblemDetails(e)]; + } + } + + /** + * Wrap the {execute} function in a try/catch block and return the result. + * If the executor function throws an error, then show the details in a notification. + */ + async executeAndNotify( + func: CancelablePromise, + options?: UmbNotificationOptions + ): Promise { + try { + return await this.execute(func); + } catch (e) { + const error = this.#toProblemDetails(e); + if (error) { + const data: UmbNotificationDefaultData = { + headline: error.title ?? 'Server Error', + message: error.detail ?? 'Something went wrong', + }; + this._notificationService?.peek('danger', { data, ...options }); + } + } + + return undefined; + } + + /** + * Cancel all resources that are currently being executed. + */ + cancelAllResources() { + this.#promises.forEach((promise) => { + if (promise instanceof CancelablePromise) { + promise.cancel(); + } + }); + } + + /** + * Extract the ProblemDetails object from an ApiError. + * + * This assumes that all ApiErrors contain a ProblemDetails object in their body. + */ + #toProblemDetails(error: unknown): ProblemDetails | undefined { + if (error instanceof ApiError) { + const errorDetails = error.body as ProblemDetails; + return errorDetails; + } + + return undefined; + } + } + + return UmbResourceMixinClass as unknown as HTMLElementConstructor & T; +};