UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

218 lines (211 loc) 18.2 kB
import * as i0 from '@angular/core'; import { InjectionToken, Injectable, inject, DestroyRef, Component } from '@angular/core'; import * as i1 from '@c8y/ngx-components'; import { hookGeneric, ExtensionPointForPlugins, fromTriggerOnce, getInjectedHooks, ModalModule, C8yTranslateModule, CommonModule, CoreModule } from '@c8y/ngx-components'; import { distinctUntilChanged, shareReplay, withLatestFrom, startWith } from 'rxjs'; import { BsModalService } from 'ngx-bootstrap/modal'; import * as i2 from '@angular/forms'; import { FormBuilder, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { gettext } from '@c8y/ngx-components/gettext'; import { TranslateService } from '@ngx-translate/core'; import { DeviceShellService } from '@c8y/ngx-components/device-shell'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import * as i3 from '@angular/common'; /** * Injection token for registering additional operation templates. * * Use the `hookOperationTemplate` helper to register templates from a plugin: * * ```typescript * // Provide a single template * hookOperationTemplate({ name: 'Restart device', command: { c8y_Restart: {} } }) * * // Provide multiple templates * hookOperationTemplate([ * { name: 'Restart device', command: { c8y_Restart: {} } }, * { name: 'Set relay OPEN', command: { c8y_Relay: { relayState: 'OPEN' } } } * ]) * ``` */ const HOOK_OPERATION_TEMPLATE = new InjectionToken('HOOK_OPERATION_TEMPLATE'); /** * Registers one or more operation templates to be shown in the operation modal selector. * * @example * ```typescript * \@NgModule({ * providers: [ * hookOperationTemplate({ name: 'Restart device', command: { c8y_Restart: {} } }) * ] * }) * export class MyPlugin {} * ``` */ function hookOperationTemplate(template, options) { return hookGeneric(template, HOOK_OPERATION_TEMPLATE, options); } class OperationTemplateService extends ExtensionPointForPlugins { constructor(rootInjector, pluginService) { super(rootInjector, pluginService); this.items$ = this.setupItemsObservable(); } get state() { return this.state$.value; } setupItemsObservable() { return fromTriggerOnce(undefined, this.refresh$, [ getInjectedHooks(HOOK_OPERATION_TEMPLATE, this.injectors), () => this.factories ]).pipe(distinctUntilChanged(), shareReplay(1)); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OperationTemplateService, deps: [{ token: i0.Injector }, { token: i1.PluginsResolveService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OperationTemplateService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OperationTemplateService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i0.Injector }, { type: i1.PluginsResolveService }] }); class OperationModalComponent { constructor() { this.showButtonLabelOnly = false; this.initialConfig = {}; this.deviceTypes = []; this.templates = []; this.predefinedOperations = [ { name: gettext('Restart device'), command: { description: gettext('Restart device'), c8y_Restart: {} } }, { name: gettext('Change relay status to OPEN'), command: { description: gettext('Change relay status to OPEN.'), c8y_Relay: { relayState: 'OPEN' } } }, { name: gettext('Change relay status to CLOSED'), command: { description: gettext('Change relay status to CLOSED.'), c8y_Relay: { relayState: 'CLOSED' } } } ]; this.labels = { ok: gettext('Save'), cancel: gettext('Cancel') }; this.result = new Promise((resolve, reject) => { this._save = resolve; this._cancel = reject; }); this.fb = inject(FormBuilder); this.translate = inject(TranslateService); this.deviceShellService = inject(DeviceShellService); this.operationTemplateService = inject(OperationTemplateService); this.destroyRef = inject(DestroyRef); } get modalTitle() { if (this.showButtonLabelOnly) { return this.translate.instant(gettext('Maintenance mode')); } const buttonLabel = this.form?.get('buttonLabel')?.value; return buttonLabel ? this.translate.instant(gettext('Edit operation')) : this.translate.instant(gettext('Create operation')); } async ngOnInit() { const defaultCommand = JSON.stringify({ description: 'Command description', c8y_Command: { text: '<command>' } }, null, 2); this.form = this.fb.group({ buttonLabel: new FormControl(this.initialConfig?.buttonLabel ?? '', { validators: [Validators.required] }), operation: new FormControl(this.initialConfig?.operationType ?? null), command: new FormControl(this.initialConfig?.command ?? defaultCommand, { validators: [Validators.required] }) }); if (this.deviceTypes?.length) { const loadedTemplates = await this.deviceShellService.getCommandTemplatesForDeviceType(this.deviceTypes); this.templates = loadedTemplates.map(t => ({ name: t.name, text: t.text, category: t.category })); } this.pluginTemplates$ = this.operationTemplateService.items$; this.form .get('operation') ?.valueChanges.pipe(withLatestFrom(this.pluginTemplates$.pipe(startWith([]))), takeUntilDestroyed(this.destroyRef)) .subscribe(([selectedName, pluginTemplates]) => { const predefined = [...this.predefinedOperations, ...pluginTemplates].find(p => p.name === selectedName); if (predefined) { this.form.get('command')?.setValue(JSON.stringify(predefined.command, null, 2)); return; } const template = this.templates.find(t => t.name === selectedName); if (template) { const cmd = JSON.stringify({ description: template.name, c8y_Command: { text: template.text } }, null, 2); this.form.get('command')?.setValue(cmd); } }); } onClose(_) { if (this.form.valid) { this._save(this.form.getRawValue()); } } onDismiss(_) { this._cancel(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OperationModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: OperationModalComponent, isStandalone: true, selector: "c8y-operation-render-type-modal", providers: [DeviceShellService], ngImport: i0, template: "<c8y-modal\n [title]=\"modalTitle\"\n (onClose)=\"onClose($event)\"\n (onDismiss)=\"onDismiss($event)\"\n [labels]=\"labels\"\n [disabled]=\"form?.invalid\"\n [headerClasses]=\"'dialog-header'\"\n>\n <ng-container c8y-modal-title>\n <span [c8yIcon]=\"'cog'\"></span>\n </ng-container>\n <form\n class=\"p-24\"\n [formGroup]=\"form\"\n >\n <c8y-form-group>\n <label\n for=\"buttonLabel\"\n translate\n >\n Button label\n </label>\n <input\n class=\"form-control\"\n id=\"buttonLabel\"\n placeholder=\"{{ 'e.g. Execute operation' | translate }}\"\n type=\"text\"\n formControlName=\"buttonLabel\"\n />\n <c8y-messages></c8y-messages>\n </c8y-form-group>\n\n @if (!showButtonLabelOnly) {\n <div class=\"form-group\">\n <label\n for=\"operation\"\n translate\n >\n Operation\n </label>\n <div class=\"c8y-select-wrapper\">\n <select\n class=\"form-control\"\n id=\"operation\"\n formControlName=\"operation\"\n >\n <optgroup label=\"{{ 'Predefined' | translate }}\">\n @for (op of predefinedOperations; track op.name) {\n <option [ngValue]=\"op.name\">{{ op.name | translate }}</option>\n }\n </optgroup>\n @let pluginTemplates = pluginTemplates$ | async;\n @if (pluginTemplates?.length) {\n <optgroup label=\"{{ 'Plugin templates' | translate }}\">\n @for (op of pluginTemplates; track op.name) {\n <option [ngValue]=\"op.name\">{{ op.name | translate }}</option>\n }\n </optgroup>\n }\n @if (templates.length) {\n <optgroup label=\"{{ 'Device templates' | translate }}\">\n @for (op of templates; track op.name) {\n <option [ngValue]=\"op.name\">{{ op.name | translate }}</option>\n }\n </optgroup>\n }\n </select>\n </div>\n </div>\n <div class=\"form-group\">\n <label\n for=\"command\"\n translate\n >\n Command\n </label>\n <textarea\n class=\"form-control no-resize inner-scroll\"\n style=\"max-height: 300px\"\n id=\"command\"\n c8y-textarea-autoresize\n formControlName=\"command\"\n maxlength=\"900\"\n ></textarea>\n </div>\n }\n </form>\n</c8y-modal>\n", dependencies: [{ kind: "ngmodule", type: ModalModule }, { kind: "component", type: i1.ModalComponent, selector: "c8y-modal", inputs: ["disabled", "close", "dismiss", "title", "body", "customFooter", "headerClasses", "labels"], outputs: ["onDismiss", "onClose"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: C8yTranslateModule }, { kind: "directive", type: i1.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i1.TextareaAutoresizeDirective, selector: "[c8y-textarea-autoresize]" }, { kind: "ngmodule", type: CoreModule }, { kind: "component", type: i1.FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "component", type: i1.MessagesComponent, selector: "c8y-messages", inputs: ["show", "defaults", "helpMessage", "additionalMessages"] }, { kind: "directive", type: i1.RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: i3.AsyncPipe, name: "async" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OperationModalComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-operation-render-type-modal', standalone: true, imports: [ ModalModule, FormsModule, ReactiveFormsModule, C8yTranslateModule, CommonModule, CoreModule ], providers: [DeviceShellService], template: "<c8y-modal\n [title]=\"modalTitle\"\n (onClose)=\"onClose($event)\"\n (onDismiss)=\"onDismiss($event)\"\n [labels]=\"labels\"\n [disabled]=\"form?.invalid\"\n [headerClasses]=\"'dialog-header'\"\n>\n <ng-container c8y-modal-title>\n <span [c8yIcon]=\"'cog'\"></span>\n </ng-container>\n <form\n class=\"p-24\"\n [formGroup]=\"form\"\n >\n <c8y-form-group>\n <label\n for=\"buttonLabel\"\n translate\n >\n Button label\n </label>\n <input\n class=\"form-control\"\n id=\"buttonLabel\"\n placeholder=\"{{ 'e.g. Execute operation' | translate }}\"\n type=\"text\"\n formControlName=\"buttonLabel\"\n />\n <c8y-messages></c8y-messages>\n </c8y-form-group>\n\n @if (!showButtonLabelOnly) {\n <div class=\"form-group\">\n <label\n for=\"operation\"\n translate\n >\n Operation\n </label>\n <div class=\"c8y-select-wrapper\">\n <select\n class=\"form-control\"\n id=\"operation\"\n formControlName=\"operation\"\n >\n <optgroup label=\"{{ 'Predefined' | translate }}\">\n @for (op of predefinedOperations; track op.name) {\n <option [ngValue]=\"op.name\">{{ op.name | translate }}</option>\n }\n </optgroup>\n @let pluginTemplates = pluginTemplates$ | async;\n @if (pluginTemplates?.length) {\n <optgroup label=\"{{ 'Plugin templates' | translate }}\">\n @for (op of pluginTemplates; track op.name) {\n <option [ngValue]=\"op.name\">{{ op.name | translate }}</option>\n }\n </optgroup>\n }\n @if (templates.length) {\n <optgroup label=\"{{ 'Device templates' | translate }}\">\n @for (op of templates; track op.name) {\n <option [ngValue]=\"op.name\">{{ op.name | translate }}</option>\n }\n </optgroup>\n }\n </select>\n </div>\n </div>\n <div class=\"form-group\">\n <label\n for=\"command\"\n translate\n >\n Command\n </label>\n <textarea\n class=\"form-control no-resize inner-scroll\"\n style=\"max-height: 300px\"\n id=\"command\"\n c8y-textarea-autoresize\n formControlName=\"command\"\n maxlength=\"900\"\n ></textarea>\n </div>\n }\n </form>\n</c8y-modal>\n" }] }] }); class OperationPickerService { constructor() { this.modalService = inject(BsModalService); } /** * * @param options Modal configuration options * `showButtonLabelOnly` - if `true`, only button label will be shown in the modal, otherwise user will be able to select both operation and command. This is useful when you want to allow users to select only predefined operations (e.g. restart, shutdown) without showing them the underlying operation types and commands. * `deviceTypes` - list of device types to filter available operations. Only operations applicable to the provided device types will be shown in the modal. * `initialConfig` - initial configuration for the modal form. This is useful when you want to edit existing configuration, so you can prefill the form with existing values. * @returns */ async openModal(options) { const modal = this.modalService.show(OperationModalComponent, { class: 'modal-sm', ariaDescribedby: 'modal-body', ariaLabelledBy: 'modal-title', ignoreBackdropClick: true, initialState: { initialConfig: options.initialConfig ?? {}, showButtonLabelOnly: options.showButtonLabelOnly, deviceTypes: options.deviceTypes } }).content; return modal.result; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OperationPickerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OperationPickerService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OperationPickerService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * Generated bundle index. Do not edit. */ export { OperationPickerService, OperationTemplateService }; //# sourceMappingURL=c8y-ngx-components-operation-picker.mjs.map