desktop/src/@batch-flask/ui/entity-commands/entity-command.ts (254 lines of code) (raw):

import { Injector } from "@angular/core"; import { I18nService, ServerError, TelemetryService } from "@batch-flask/core"; import { ListSelection } from "@batch-flask/core/list"; import { Activity, ActivityService, ActivityStatus } from "@batch-flask/ui/activity"; import { DialogService } from "@batch-flask/ui/dialogs"; import { NotificationService } from "@batch-flask/ui/notifications"; import { Permission } from "@batch-flask/ui/permission"; import { WorkspaceService } from "@batch-flask/ui/workspace"; import { exists, log, nil } from "@batch-flask/utils"; import * as inflection from "inflection"; import { Observable, forkJoin, of } from "rxjs"; import { map, share } from "rxjs/operators"; import { ActionableEntity, EntityCommands, ExecuteOptions } from "./entity-commands"; export enum EntityCommandNotify { Always, Never, OnFailure, } export interface EntityCommandAttributes<TEntity extends ActionableEntity, TOptions = void> { /** * Name is to be used by feature to show buttons. Or to define keyboard shortcuts */ name: string; label: ((entity: TEntity) => string) | string; icon?: ((entity: TEntity) => string) | string; action: (entity: TEntity, option?: TOptions) => Observable<any> | void; enabled?: (entity: TEntity) => boolean; visible?: (entity: TEntity) => boolean; multiple?: boolean | ((entities: TEntity[], options?: any) => Observable<any>); confirm?: ((entities: TEntity[]) => Observable<TOptions>) | boolean; notify?: EntityCommandNotify | boolean; permission?: Permission; } /** * Entity command is a commnad available to an entity */ export class EntityCommand<TEntity extends ActionableEntity, TOptions = void> { public name: string; public notify: EntityCommandNotify; public multiple: boolean | ((entities: TEntity[], options?: any) => Observable<any>); public enabled: (entity: TEntity) => boolean; public confirm: ((entities: TEntity[]) => Observable<TOptions>) | boolean; public definition: EntityCommands<TEntity>; public permission: Permission; public feature: string; private _action: (entity: TEntity, option?: TOptions) => Observable<any> | void; private _label: ((entity: TEntity) => string) | string; private _icon: ((entity: TEntity) => string) | string; private _visible: (entity: TEntity) => boolean; // Services private dialogService: DialogService; private notificationService: NotificationService; private activityService: ActivityService; private workspaceService: WorkspaceService; private i18n: I18nService; private telemetryService: TelemetryService; constructor(injector: Injector, attributes: EntityCommandAttributes<TEntity, TOptions>) { this.notificationService = injector.get(NotificationService); this.dialogService = injector.get(DialogService); this.activityService = injector.get(ActivityService); this.workspaceService = injector.get(WorkspaceService); this.i18n = injector.get(I18nService); this.telemetryService = injector.get(TelemetryService); this.name = attributes.name; this._label = attributes.label; this._icon = attributes.icon || "fa fa-question"; this._action = attributes.action; this.multiple = exists(attributes.multiple) ? attributes.multiple : true; this.enabled = attributes.enabled || (() => true); this._visible = attributes.visible || (() => true); this.confirm = exists(attributes.confirm) ? attributes.confirm : true; this.permission = attributes.permission || Permission.Read; if (attributes.notify === true || nil(attributes.notify)) { this.notify = EntityCommandNotify.Always; } else if (attributes.notify === false) { this.notify = EntityCommandNotify.Never; } else { this.notify = attributes.notify; } } public label(entity: TEntity) { return this._label instanceof Function ? this._label(entity) : this._label; } public icon(entity: TEntity) { return this._icon instanceof Function ? this._icon(entity) : this._icon; } public disabled(entity: TEntity) { return !this.enabled(entity); } public visible(entity: TEntity) { return this._visible(entity) && this._isFeatureEnabled(); } public performAction(entity: TEntity, option: TOptions): Observable<any> { const obs = this._action(entity, option); if (!obs) { return of(null); } return obs; } public performActionAndRefresh(entity: TEntity, option: TOptions): Observable<any> { const obs = this.performAction(entity, option); obs.subscribe({ complete: () => { this.definition.get(entity.id).subscribe({ error: () => null, }); }, error: () => { this.definition.get(entity.id).subscribe({ error: () => null, }); }, }); return obs; } /** * Try to execute the command for the given entity. * This will ask for confirmation unless command explicity configured not to */ public execute(entity: TEntity, options: ExecuteOptions = {}) { this._trackAction(1); if (this.confirm && !options.skipConfirm) { if (this.confirm instanceof Function) { this.confirm([entity]).subscribe((options) => { this._executeCommand(entity, options); }); } else { const label = this.label(entity); const type = this.definition.typeName.toLowerCase(); const message = this.i18n.t("entity-command.confirm.single.title", { action: label.toLowerCase(), type, }); const description = this.i18n.t("entity-command.confirm.single.description", { action: label.toLowerCase(), entityId: entity.id, }); this.dialogService.confirm(message, { description, yes: () => { this._executeCommand(entity); }, }); } } else { this._executeCommand(entity); } } /** * Try to execute the command for the given entities. * This will ask for confirmation unless command explicity configured not to */ public executeMultiple(entities: TEntity[], options: ExecuteOptions = {}) { this._trackAction(entities.length); if (this.confirm && !options.skipConfirm) { if (this.confirm instanceof Function) { this.confirm(entities).subscribe((options) => { this._executeMultiple(entities, options); }); } else { const type = inflection.pluralize(this.definition.typeName.toLowerCase()); const label = this.label(entities.first()); const message = this.i18n.t("entity-command.confirm.multiple.title", { action: label.toLowerCase(), count: entities.length, type, }); this.dialogService.confirm( message, { description: entities.map(x => x.id).join("\n"), yes: () => { this._executeMultiple(entities); }, }); } } else { this._executeMultiple(entities); } } public executeMultipleByIds(ids: string[]) { const obs = ids.map(id => this.definition.getFromCache(id)); return forkJoin(obs).pipe( map(entities => this.executeMultiple(entities)), share(), ); } public executeFromSelection(selection: ListSelection) { return this.executeMultipleByIds([...selection.keys]); } private _executeCommand(entity: TEntity, options?: any) { this._trackConfirm(1); const label = this.label(entity); this.performActionAndRefresh(entity, options).subscribe({ next: () => { this._notifySuccess(`${label} was successful.`, `${entity.name}`); }, error: (e: ServerError) => { this._notifyError(`${label} failed.`, `${entity.name} ${e.message}`); log.error(`Failed to execute command ${label} for entity ${entity.name}`, e); }, }); } private _executeMultiple(entities: TEntity[], options?: any) { this._trackConfirm(entities.length); const label = this.label(entities[0]); if (typeof this.multiple === "function") { return this.multiple(entities, options).subscribe({ next: () => { this._notifySuccess(`${label} was successful.`, ""); }, error: (e: ServerError) => { this._notifyError(`${label} failed.`, e.message); }, }); } const enabledEntities = entities.filter(x => this.enabled(x)); const type = inflection.pluralize(this.definition.typeName.toLowerCase()); // create an activity that creates a list of subactivities const activity = new Activity(`${label} ${type}`, () => { // create a subactivity for each enabled entity const subActivities = enabledEntities.map((entity) => { return new Activity(`${label} ${entity.name}`, () => { // each subactivity should perform an action and refresh return this.performActionAndRefresh(entity, options); }); }); return of(subActivities); }); // notify success after the parent activity completes activity.done.subscribe((status) => { if (status === ActivityStatus.Completed) { this._notifySuccess(`${label} was successful.`, ""); } }); // run the parent activity this.activityService.exec(activity); } private _notifySuccess(message: string, description: string) { if (this.notify === EntityCommandNotify.Always) { this.notificationService.success(message, description); } } private _notifyError(message: string, description: string) { if (this.notify !== EntityCommandNotify.Never) { this.notificationService.error(message, description); } } private _isFeatureEnabled(): boolean { const feature = this.definition.config.feature; if (!feature) { return true; } return this.workspaceService.isFeatureEnabled(`${feature}.${this.name}`); } private _trackAction(count: number) { this.telemetryService.trackEvent({ name: "Execute action", properties: { type: this.definition.typeName, name: this.name, count, }, }); } private _trackConfirm(count: number) { this.telemetryService.trackEvent({ name: "Execute action confirmed", properties: { type: this.definition.typeName, name: this.name, count, }, }); } }