lp1855780 Notification Action Triggers
authorMike Risher <mrisher@catalyte.io>
Thu, 19 Dec 2019 17:56:41 +0000 (17:56 +0000)
committerChris Sharp <csharp@georgialibraries.org>
Tue, 3 Mar 2020 15:26:46 +0000 (10:26 -0500)
Port Notification Action Triggers from DOJO to Angular.  This consists of 4 grids, each
navigated to by its corresponding tab. The Trigger Event Definitions grid allows
cloning of records.

Signed-off-by: Mike Risher <mrisher@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html
modified:   Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers.component.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers.module.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers_routing.module.ts

Signed-off-by: Chris Sharp <csharp@georgialibraries.org>
Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts
Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers_routing.module.ts [new file with mode: 0644]

index 223d181..a4d2e2d 100644 (file)
@@ -43,7 +43,7 @@
     <eg-link-table-link i18n-label label="Non-Cataloged Types Editor" 
       routerLink="/staff/admin/local/config/non_cataloged_type"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Notifications / Action Triggers" 
-      url="/eg/staff/admin/local/action_trigger/event_definition"></eg-link-table-link>
+      routerLink="/staff/admin/local/action_trigger/event_definition"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Patrons with Negative Balances" 
       url="/eg/staff/admin/local/circ/neg_balance_users"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Permission Tree Display Entries" 
index 15a9153..e353be7 100644 (file)
@@ -14,6 +14,9 @@ const routes: Routes = [{
     component: BasicAdminPageComponent,
     data: [{schema: 'config', table: 'hold_matrix_matchpoint', disableOrgFilter: true}]
 }, {
+    path: 'action_trigger/event_definition',
+    loadChildren: '@eg/staff/admin/local/triggers/triggers.module#TriggersModule'
+}, {
     path: 'actor/address_alert',
     component: AddressAlertComponent
 }, {
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers.component.html
new file mode 100644 (file)
index 0000000..97abe71
--- /dev/null
@@ -0,0 +1,92 @@
+<eg-staff-banner bannerText="Local Admin Notifications/Action Triggers" i18n-bannerText>
+</eg-staff-banner>
+
+<ng-template #textAreaTemplate let-field="field" let-record="record">
+    <textarea class="form-control" name="{{field.name}}" [readonly]="field.readOnly"
+    [required]="field.isRequired()" [ngModel]="record[field.name]()" 
+    (ngModelChange)="record[field.name]($event)" style="height: 600px;">
+    </textarea>
+</ng-template>
+<eg-fm-record-editor #eventDialog idlClass="atevdef"
+[fieldOptions]="{message_template:{customTemplate:{template:textAreaTemplate}},template:{customTemplate:{template:textAreaTemplate}}}"
+fieldOrder="owner,name,hook,active,delay,delay_field,group_field,reactor,validator,repeat_delay,id,cleanup_failure,granularity,max_delay,message_library_path,message_template,message_title,message_usr_path,opt_in_setting,usr_field,retention_interval,cleanup_success,template">
+</eg-fm-record-editor>  
+<eg-fm-record-editor #hookDialog idlClass="ath"></eg-fm-record-editor>        
+<eg-fm-record-editor #reactorDialog idlClass="atreact"></eg-fm-record-editor>        
+<eg-fm-record-editor #validatorDialog idlClass="atval"></eg-fm-record-editor>        
+
+<ngb-tabset #triggersTabs [activeId]="triggerTab"  (tabChange)="onTabChange($event)"
+    class="mb-3">    
+    <ngb-tab title="Event Definitions" i18n-title id="101">        
+        <ng-template ngbTabContent>
+            <h4 class="mb-3 mt-3">Trigger Event Definitions</h4>
+            <eg-grid #eventsGrid idlClass="atevdef" [dataSource]="eventsDataSource" 
+            showFields="owner,name,hook,active,delay,delay_field,group_field,reactor,validator,repeat_delay,granularity,retention_interval"
+            [stickyHeader]="true" [showLinkSelectors]="true" [sortable]="true" 
+            (onRowActivate)="editSelected([$event])" [filterable]="true">
+                <eg-grid-toolbar-button label="New Event Definition" i18n-label
+                [action]="createNewEvent"></eg-grid-toolbar-button>
+                <eg-grid-toolbar-action label="Edit Selected" i18n-label 
+                [action]="editSelected"></eg-grid-toolbar-action>
+                <eg-grid-toolbar-action label="Clone Selected" i18n-label 
+                [action]="cloneSelected"></eg-grid-toolbar-action>
+                <eg-grid-toolbar-action label="Delete Selected" i18n-label 
+                (onClick)="deleteSelected($event)"></eg-grid-toolbar-action>
+            </eg-grid>
+        </ng-template>
+    </ngb-tab>
+    <ngb-tab title="Hooks" i18n-title id="hooks">
+        <ng-template ngbTabContent>
+            <h4 class="mb-3 mt-3">Trigger Hooks</h4>
+            <eg-grid #hooksGrid idlClass="ath" [dataSource]="hooksDataSource"
+            (onRowActivate)="editSelected([$event])" [sortable]="true" [filterable]="true">
+                <eg-grid-toolbar-button label="New Hook" i18n-label [action]="createNewHook">
+                </eg-grid-toolbar-button>
+                <eg-grid-toolbar-action label="Edit Selected" i18n-label 
+                [action]="editSelected"></eg-grid-toolbar-action>
+                <eg-grid-toolbar-action label="Delete Selected" i18n-label 
+                (onClick)="deleteSelected($event)"></eg-grid-toolbar-action>
+            </eg-grid>
+        </ng-template>
+    </ngb-tab>
+    <ngb-tab title="Reactors" i18n-title id="reactors">
+        <ng-template ngbTabContent>
+            <h4 class="mb-3 mt-3">Trigger Reactors</h4>
+            <eg-grid #reactorsGrid idlClass="atreact" [dataSource]="reactorsDataSource"
+            (onRowActivate)="editSelected([$event])" [sortable]="true" [filterable]="true">
+                <eg-grid-toolbar-button label="New Reactor" i18n-label 
+                [action]="createNewReactor"></eg-grid-toolbar-button>
+                <eg-grid-toolbar-action label="Edit Selected" i18n-label 
+                [action]="editSelected"></eg-grid-toolbar-action>
+                <eg-grid-toolbar-action label="Delete Selected" i18n-label 
+                (onClick)="deleteSelected($event)"></eg-grid-toolbar-action>
+            </eg-grid>
+        </ng-template>
+    </ngb-tab>
+    <ngb-tab title="Validators" i18n-title id="validators">
+        <ng-template ngbTabContent>
+            <h4 class="mb-3 mt-3">Trigger Validators</h4>
+            <eg-grid #validatorsGrid idlClass="atval" [dataSource]="validatorsDataSource"
+            (onRowActivate)="editSelected([$event])" [sortable]="true" [filterable]="true">
+                <eg-grid-toolbar-button label="New Validator" i18n-label 
+                [action]="createNewValidator"></eg-grid-toolbar-button>
+                <eg-grid-toolbar-action label="Edit Selected" i18n-label 
+                [action]="editSelected"></eg-grid-toolbar-action>
+                <eg-grid-toolbar-action label="Delete Selected" i18n-label 
+                (onClick)="deleteSelected($event)"></eg-grid-toolbar-action>
+            </eg-grid>
+        </ng-template>
+    </ngb-tab>
+</ngb-tabset>
+        
+<eg-string #createSuccessString i18n-text text="New entry Added"></eg-string>
+<eg-string #createErrString i18n-text text="Failed to create new entry"></eg-string>
+<eg-string #deleteFailedString i18n-text text="Delete of entry failed or was not allowed">
+</eg-string>
+<eg-string #deleteSuccessString i18n-text text="Delete of entry succeeded"></eg-string>
+<eg-string #updateFailedString i18n-text text="Update of entry failed or was not allowed">
+</eg-string>
+<eg-string #updateSuccessString i18n-text text="Update of entry succeeded"></eg-string>
+<eg-string #cloneFailedString i18n-text text="Clone of entry failed or was not allowed">
+</eg-string>
+<eg-string #cloneSuccessString i18n-text text="Clone of entry succeeded"></eg-string>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers.component.ts
new file mode 100644 (file)
index 0000000..69ad7db
--- /dev/null
@@ -0,0 +1,240 @@
+import {Pager} from '@eg/share/util/pager';
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+    templateUrl: './triggers.component.html'
+})
+
+export class TriggersComponent implements OnInit {
+
+    eventsDataSource: GridDataSource = new GridDataSource();
+    hooksDataSource: GridDataSource = new GridDataSource();
+    reactorsDataSource: GridDataSource = new GridDataSource();
+    validatorsDataSource: GridDataSource = new GridDataSource();
+    triggerTab: string;
+    idlClass: string;
+
+    @ViewChild('eventDialog', {static: false}) eventDialog: FmRecordEditorComponent;
+    @ViewChild('hookDialog', {static: false}) hookDialog: FmRecordEditorComponent;
+    @ViewChild('reactorDialog', {static: false}) reactorDialog: FmRecordEditorComponent;
+    @ViewChild('validatorDialog', {static: false}) validatorDialog: FmRecordEditorComponent;
+
+    @ViewChild('eventsGrid', {static: false}) eventsGrid: GridComponent;
+    @ViewChild('hooksGrid', {static: false}) hooksGrid: GridComponent;
+    @ViewChild('reactorsGrid', {static: false}) reactorsGrid: GridComponent;
+    @ViewChild('validatorsGrid', {static: false}) validatorsGrid: GridComponent;
+
+    @ViewChild('updateSuccessString', {static: false}) updateSuccessString: StringComponent;
+    @ViewChild('updateFailedString', {static: false}) updateFailedString: StringComponent;
+    @ViewChild('cloneSuccessString', {static: false}) cloneSuccessString: StringComponent;
+    @ViewChild('cloneFailedString', {static: false}) cloneFailedString: StringComponent;
+    @ViewChild('deleteFailedString', {static: false}) deleteFailedString: StringComponent;
+    @ViewChild('deleteSuccessString', {static: false}) deleteSuccessString: StringComponent;
+    @ViewChild('createSuccessString', {static: false}) createSuccessString: StringComponent;
+    @ViewChild('createErrString', {static: false}) createErrString: StringComponent;
+
+    constructor(
+        private idl: IdlService,
+        private pcrud: PcrudService,
+        private toast: ToastService,
+    ) {
+    }
+
+    ngOnInit() {
+        this.eventsDataSource.getRows = (pager: Pager, sort: any[]) => {
+            const orderEventsBy: any = {atevdef: 'name'};
+            if (sort.length) {
+                orderEventsBy.atevdef = sort[0].name + ' ' + sort[0].dir;
+            }
+            return this.doSearch('atevdef', orderEventsBy, this.eventsDataSource, pager);
+        };
+
+        this.hooksDataSource.getRows = (pager: Pager, sort: any[]) => {
+            const orderHooksBy: any = {ath: 'key'};
+            if (sort.length) {
+                orderHooksBy.ath = sort[0].name + ' ' + sort[0].dir;
+            }
+            return this.doSearch('ath', orderHooksBy, this.reactorsDataSource, pager);
+        };
+
+        this.reactorsDataSource.getRows = (pager: Pager, sort: any[]) => {
+            const orderReactorsBy: any = {atreact: 'module'};
+            if (sort.length) {
+                orderReactorsBy.atreact = sort[0].name + ' ' + sort[0].dir;
+            }
+            return this.doSearch('atreact', orderReactorsBy, this.reactorsDataSource, pager);
+        };
+
+        this.validatorsDataSource.getRows = (pager: Pager, sort: any[]) => {
+            const orderValidatorsBy: any = {atval: 'module'};
+            if (sort.length) {
+                orderValidatorsBy.atval = sort[0].name + ' ' + sort[0].dir;
+            }
+            return this.doSearch('atval', orderValidatorsBy, this.validatorsDataSource, pager);
+        };
+    }
+
+    doSearch(idlString: any, currentOrderBy: any, currentDataSource: any, pager: Pager) {
+        const base: Object = {};
+        base[this.idl.classes[idlString].pkey] = {'!=' : null};
+        const query: any = new Array();
+        query.push(base);
+        Object.keys(currentDataSource.filters).forEach(key => {
+            Object.keys(currentDataSource.filters[key]).forEach(key2 => {
+                query.push(currentDataSource.filters[key][key2]);
+            });
+        });
+        return this.pcrud.search(idlString,
+            query, {
+            offset: pager.offset,
+            limit: pager.limit,
+            order_by: currentOrderBy
+        });
+    }
+
+    onTabChange(event: NgbTabChangeEvent) {
+        this.triggerTab = event.nextId;
+    }
+
+    createNewEvent = () => {
+        this.createNewThing(this.eventDialog, this.eventsGrid);
+    }
+
+    createNewHook = () => {
+        this.createNewThing(this.hookDialog, this.hooksGrid);
+    }
+
+    createNewReactor = () => {
+        this.createNewThing(this.reactorDialog, this.reactorsGrid);
+    }
+
+    createNewValidator = () => {
+        this.createNewThing(this.validatorDialog, this.validatorsGrid);
+    }
+
+    createNewThing = (currentDialog: any, currentGrid: any) => {
+        currentDialog.mode = 'create';
+        currentDialog.recordId = null;
+        currentDialog.record = null;
+        currentDialog.open({size: 'lg'}).subscribe(
+            ok => {
+                this.createSuccessString.current()
+                    .then(str => this.toast.success(str));
+                currentGrid.reload();
+            },
+            rejection => {
+                if (!rejection.dismissed) {
+                    this.createErrString.current()
+                        .then(str => this.toast.danger(str));
+                }
+            }
+        );
+    }
+
+    editSelected = (selectedRecords: IdlObject[]) => {
+        const editOneThing = (record: IdlObject) => {
+            if (!record) { return; }
+            this.showEditDialog(record).then(
+                () => editOneThing(selectedRecords.shift()));
+        };
+        editOneThing(selectedRecords.shift());
+    }
+
+    lookUpIdl (idl: string) {
+        let currentDialog;
+        let currentGrid;
+        switch (idl) {
+            case 'atevdef':
+                currentDialog = this.eventDialog;
+                currentGrid = this.eventsGrid;
+                break;
+            case 'ath':
+                currentDialog = this.hookDialog;
+                currentGrid = this.hooksGrid;
+                break;
+            case 'atreact':
+                currentDialog = this.reactorDialog;
+                currentGrid = this.reactorsGrid;
+                break;
+            case 'atval':
+                currentDialog = this.validatorDialog;
+                currentGrid = this.validatorsGrid;
+                break;
+            default:
+                console.debug('Unknown class name');
+        }
+        return {currentDialog: currentDialog, currentGrid: currentGrid};
+    }
+
+    showEditDialog = (selectedRecord: IdlObject): Promise<any> => {
+        const idl = selectedRecord.classname;
+        const lookupResults = this.lookUpIdl(idl);
+        const currentDialog = lookupResults.currentDialog;
+        const currentGrid = lookupResults.currentGrid;
+        currentDialog.mode = 'update';
+        const clone = this.idl.clone(selectedRecord);
+        currentDialog.record = clone;
+        return new Promise((resolve, reject) => {
+            currentDialog.open({size: 'lg'}).subscribe(
+                result => {
+                    this.updateSuccessString.current()
+                        .then(str => this.toast.success(str));
+                    currentGrid.reload();
+                    resolve(result);
+                },
+                error => {
+                    this.updateFailedString.current()
+                        .then(str => this.toast.danger(str));
+                    reject(error);
+               }
+           );
+       });
+    }
+
+    deleteSelected = (idlThings: IdlObject[]) => {
+        const idl = idlThings[0].classname;
+        const currentGrid = this.lookUpIdl(idl).currentGrid;
+        idlThings.forEach(idlThing => idlThing.isdeleted(true));
+        this.pcrud.autoApply(idlThings).subscribe(
+            val => {
+                console.debug('deleted: ' + val);
+                this.deleteSuccessString.current()
+                    .then(str => this.toast.success(str));
+                currentGrid.reload();
+            },
+            err => {
+                this.deleteFailedString.current()
+                    .then(str => this.toast.danger(str));
+            }
+        );
+    }
+
+    cloneSelected = (idlThings: IdlObject[]) => {
+        const clone = this.idl.clone(idlThings[0]);
+        clone.id(null);
+        this.eventDialog.mode = 'create';
+        this.eventDialog.recordId = null;
+        this.eventDialog.record = clone;
+        this.eventDialog.open({size: 'lg'}).subscribe(
+            ok => {
+                this.cloneSuccessString.current()
+                    .then(str => this.toast.success(str));
+                this.eventsGrid.reload();
+            },
+            rejection => {
+                if (!rejection.dismissed) {
+                    this.cloneFailedString.current()
+                        .then(str => this.toast.danger(str));
+                }
+            }
+        );
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers.module.ts
new file mode 100644 (file)
index 0000000..61e982e
--- /dev/null
@@ -0,0 +1,18 @@
+import {NgModule} from '@angular/core';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {TriggersComponent} from './triggers.component';
+import {TriggersRoutingModule} from './triggers_routing.module';
+
+@NgModule({
+  declarations: [
+    TriggersComponent,
+  ],
+  imports: [
+    AdminCommonModule,
+    TriggersRoutingModule,
+  ],
+  exports: [],
+  providers: []
+})
+
+export class TriggersModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers_routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/triggers/triggers_routing.module.ts
new file mode 100644 (file)
index 0000000..297b11f
--- /dev/null
@@ -0,0 +1,15 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {TriggersComponent} from './triggers.component';
+
+const routes: Routes = [{
+    path: '',
+    component: TriggersComponent
+}];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class TriggersRoutingModule {}