lp1839341 Port Org Setting Editor UI
authorKyle Huckins <khuckins@catalyte.io>
Tue, 13 Aug 2019 15:07:07 +0000 (15:07 +0000)
committerJane Sandberg <sandbergja@gmail.com>
Thu, 13 Oct 2022 16:04:00 +0000 (09:04 -0700)
- Speedy Retrieval for display all Org Unit Settings (~6 seconds
instead of DOJO's 20)
- Implement org_unit.settings.history.retrieve API Call utilizing
CSTORE operations
- View and revert OU settings to specific changes
- Update Org Unit Setting context orgs and values
- Filtering of Org Unit Settings by string found in name, description,
label, and/or group fields of Org Unit settings
- Get history in properly descending order based on date_applied
field
- Strip surrounding quotes from new values in history log
- Add columns for Edit and History actions.
- Add sql changes to support workstation setting for org unit settings grid
- Port Import/Export Dialog for batch-modifying settings using a JSON string.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>

 Changes to be committed:
modified:   Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/edit-org-unit-setting-dialog.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/edit-org-unit-setting-dialog.component.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-history-dialog.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-history-dialog.component.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-json-dialog.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-json-dialog.component.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings-routing.module.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings.component.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings.module.ts
modified:   Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts
modified:   Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
modified:   Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
modified:   Open-ILS/src/sql/Pg/950.data.seed-values.sql
new file:   Open-ILS/src/sql/Pg/upgrade/XXXX.data.ouSettings-grid-ws-settings.sql

Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Terran McCanna <tmccanna@georgialibraries.org>
Signed-off-by: Jane Sandberg <sandbergja@gmail.com>

16 files changed:
Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/edit-org-unit-setting-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/edit-org-unit-setting-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-history-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-history-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-json-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-json-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings-routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts
Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.ouSettings-grid-ws-settings.sql [new file with mode: 0644]

index 29e42ef..e527e3b 100644 (file)
@@ -47,7 +47,7 @@
     <eg-link-table-link i18n-label label="Item Tags" 
       routerLink="/staff/admin/local/asset/copy_tag"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Library Settings Editor" 
-      url="/eg/staff/admin/local/asset/org_unit_settings"></eg-link-table-link>
+      routerLink="/staff/admin/local/asset/org_unit_settings"></eg-link-table-link>
     <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" 
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/edit-org-unit-setting-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/edit-org-unit-setting-dialog.component.html
new file mode 100644 (file)
index 0000000..bed349f
--- /dev/null
@@ -0,0 +1,152 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Edit Setting</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <div class="row justify-content-center">
+      <div class="col">
+        <h5 i18n>{{entry.label}}</h5>
+      </div>
+    </div>
+    <div class="row justify-content-center">
+        <div class="col">
+            <span i18n>{{entry.description}}</span>
+        </div>
+    </div>
+    <div class="row mt-3">
+        <div class="col-md-6">
+            <div class="input-group">
+                <div class="input-group-prepend">
+                    <div class="input-group-text" i18n>Context</div>
+                    <eg-org-select [initialOrg]="entryContext"
+                      (onChange)="entryContext = $event"></eg-org-select>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="row mt-2">
+        <div class="col-md-6">
+            <ng-container [ngSwitch]="inputType()">
+
+              <ng-container *ngSwitchCase="'integer'">
+                <div class="input-group">
+                  <div class="input-group-prepend">
+                    <div class="input-group-text" i18n>Value</div>
+                    <input
+                      class="form-control" type="number"
+                      name="entryValue"
+                      placeholder="Input a numerical value"
+                      i18n-placeholder
+                      [(ngModel)]="entryValue"/>
+                  </div>
+                </div>
+              </ng-container>
+              <ng-container *ngSwitchCase="'currency'">
+                <div class="input-group">
+                  <div class="input-group-prepend">
+                    <div class="input-group-text" i18n>Value</div>
+                    <div class="input-group-text" i18n>$</div>
+                    <input
+                      class="form-control" type="number"
+                      step="0.01"
+                      name="entryValue"
+                      placeholder="Input a monetary value"
+                      i18n-placeholder
+                      [(ngModel)]="entryValue"/>
+                  </div>
+                </div>
+              </ng-container>
+              <ng-container *ngSwitchCase="'string'">
+                <div class="input-group">
+                  <div class="input-group-prepend">
+                    <div class="input-group-text" i18n>Value</div>
+                    <input
+                      class="form-control" type="text"
+                      name="entryValue"
+                      placeholder="Input a value"
+                      i18n-placeholder
+                      [(ngModel)]="entryValue"/>
+                  </div>
+                </div>
+              </ng-container>
+              <ng-container *ngSwitchCase="'interval'">
+                <div class="input-group">
+                  <div class="input-group-prepend">
+                    <div class="input-group-text" i18n>Value</div>
+                    <input
+                      class="form-control" type="text"
+                      name="entryValue"
+                      placeholder="e.g. 1 day, 4 months"
+                      i18n-placeholder
+                      [(ngModel)]="entryValue"/>
+                  </div>
+                </div>
+              </ng-container>
+              <ng-container *ngSwitchCase="'bool'">
+                <div class="input-group">
+                  <div class="input-group-prepend">
+                    <div class="input-group-text" i18n>Value</div>
+                    <select
+                      class="custom-select" name="entryValue"
+                      placeholder="True or False" i18n-placeholder
+                      [(ngModel)]="entryValue">
+                      <option value='true' i18n>True</option>
+                      <option value='false' i18n>False</option>
+                    </select>
+                  </div>
+                </div>
+              </ng-container>
+              <ng-container *ngSwitchCase="'array'">
+                <div class="input-group">
+                  <div class="input-group-prepend">
+                    <div class="input-group-text" i18n>Value</div>
+                    <input
+                      class="form-control" type="text"
+                      name="entryValue"
+                      placeholder="Input a comma-separated list..."
+                      i18n-placeholder
+                      [(ngModel)]="entryValue"/>
+                  </div>
+                </div>
+              </ng-container>
+              <ng-container *ngSwitchCase="'link'">
+                <div class="input-group">
+                  <div class="input-group-prepend">
+                    <div class="input-group-text" i18n>Value</div>
+                      <ng-container [ngSwitch]="entry.fmClass">
+                        <ng-container *ngSwitchCase="'acpl'">
+                          <eg-combobox placeholder="Select a value" [idlClass]="entry.fmClass" idlField="name" idlIncludeLibraryInLabel="owning_lib"
+                            [asyncSupportsEmptyTermClick]="true" [displayTemplate]="fmClassLabel"
+                            (onChange)="setInputValue($event ? $event.id : null)">
+                          </eg-combobox>
+                        </ng-container>
+                        <ng-container *ngSwitchDefault>
+                          <eg-combobox placeholder="Select a value" [idlClass]="entry.fmClass" idlField="name"
+                            [asyncSupportsEmptyTermClick]="true" [displayTemplate]="fmClassLabel"
+                            (onChange)="setInputValue($event ? $event.id : null)">
+                          </eg-combobox>
+                        </ng-container>
+                      </ng-container><!-- fmClass ngSwitch -->
+                  </div>
+                </div>
+              </ng-container>
+
+            </ng-container> <!-- input type ngSwitch -->
+        </div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" 
+      (click)="delete()" i18n>Delete Setting</button>
+    <button type="button" class="btn btn-warning" 
+      (click)="update()" i18n>Update Setting</button>
+  </div>
+</ng-template>
+
+<ng-template #fmClassLabel let-r="result" i18n>
+  {{r.label}}
+</ng-template>
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/edit-org-unit-setting-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/edit-org-unit-setting-dialog.component.ts
new file mode 100644 (file)
index 0000000..ce2add0
--- /dev/null
@@ -0,0 +1,60 @@
+import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {OrgUnitSetting} from '@eg/staff/admin/local/org-unit-settings/org-unit-settings.component';
+
+@Component({
+  selector: 'eg-admin-edit-org-unit-setting-dialog',
+  templateUrl: './edit-org-unit-setting-dialog.component.html'
+})
+
+export class EditOuSettingDialogComponent extends DialogComponent {
+
+    // What OU Setting we're editing
+    entry: any = {};
+    entryValue: any;
+    entryContext: IdlObject;
+    linkedFieldOptions: IdlObject[];
+
+    constructor(
+        private auth: AuthService,
+        private net: NetService,
+        private org: OrgService,
+        private modal: NgbModal
+    ) {
+        super(modal);
+        if (!this.entry) {
+            this.entryValue = null;
+            this.entryContext = null;
+            this.linkedFieldOptions = null;
+        }
+    }
+
+    inputType() {
+        return this.entry.dataType;
+    }
+
+    setInputValue(inputValue) {
+        console.log("In Input value");
+        console.log(inputValue);
+        this.entryValue = inputValue;
+    }
+
+    getFieldClass() {
+        return this.entry.fm_class;
+    }
+
+    delete() {
+        this.close({setting: {[this.entry.name]: null}, context: this.entryContext});
+    }
+
+    update() {
+        this.close({setting: {[this.entry.name]: this.entryValue}, context: this.entryContext});
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-history-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-history-dialog.component.html
new file mode 100644 (file)
index 0000000..082fdf2
--- /dev/null
@@ -0,0 +1,39 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>History</h4>
+    <button type="button" class="close"
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <div class="row">
+      <div class="col-md-12">
+        <span i18n>{{entry.label}}</span>
+      </div>
+    </div>
+    <div class="mt-3">
+        <eg-grid #historyGrid [dataSource]="gridDataSource"
+            [disableSelect]="true" [disableMultiSelect]="true"
+            [sortable]="false">
+
+            <eg-grid-column path="id" [index]=true label="ID" i18n-label hidden></eg-grid-column>
+            <eg-grid-column path="date_applied" label="Date Changed" datatype="timestamp" i18n-label></eg-grid-column>
+            <eg-grid-column path="org.shortname" label="Location" i18n-label></eg-grid-column>
+            <eg-grid-column path="original_value_str" label="Original Value" i18n-label></eg-grid-column>
+            <eg-grid-column path="new_value_str" label="New Value" i18n-label></eg-grid-column>
+            <eg-grid-column i18n-label label="Revert?" name="revert" 
+                [cellTemplate]="revertTemplate"></eg-grid-column>
+        </eg-grid>
+      </div>
+  </div>
+  <ng-template #revertTemplate let-log="row">
+      <span>
+        <a
+          (click)="revert(log)" class="pl-1"
+          [routerLink]="" i18n>
+          Revert
+        </a>
+      </span>
+    </ng-template>
+</ng-template>
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-history-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-history-dialog.component.ts
new file mode 100644 (file)
index 0000000..62823d3
--- /dev/null
@@ -0,0 +1,67 @@
+import {Component, Input, ViewChild, OnInit, TemplateRef} from '@angular/core';
+import {Observable, Observer, of} from 'rxjs';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {Pager} from '@eg/share/util/pager';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridToolbarCheckboxComponent
+    } from '@eg/share/grid/grid-toolbar-checkbox.component';
+import {OrgUnitSetting} from '@eg/staff/admin/local/org-unit-settings/org-unit-settings.component';
+
+@Component({
+    selector: 'eg-admin-ou-setting-history-dialog',
+    templateUrl: './org-unit-setting-history-dialog.component.html'
+})
+
+export class OuSettingHistoryDialogComponent extends DialogComponent {
+
+    entry: any = {};
+    history: any[] = [];
+    gridDataSource: GridDataSource;
+    @ViewChild('historyGrid', { static:true }) historyGrid: GridComponent;
+
+
+    constructor(
+        private auth: AuthService,
+        private net: NetService,
+        private org: OrgService,
+        private modal: NgbModal
+    ) {
+        super(modal);
+        this.gridDataSource = new GridDataSource();
+    }
+
+    ngOnInit() {
+        this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+            return this.fetchHistory(pager);
+        };
+    }
+
+    fetchHistory(pager: Pager): Observable<any> {
+        return new Observable<any>(observer => {
+            this.gridDataSource.data = this.history;
+            observer.complete();
+        });
+    }
+
+    revert(log) {
+        if (log) {
+            var intTypes = ["integer", "currency", "link"];
+            if (intTypes.includes(this.entry.dataType)) {
+                log.new_value = parseInt(log.new_value);
+            } else {
+                log.new_value = log.new_value.replace(/^"(.*)"$/, '$1');
+            }
+            this.close({
+                setting: {[this.entry.name]: log.new_value},
+                context: this.org.get(log.org),
+                revert: true
+            });
+            this.gridDataSource.data = null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-json-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-json-dialog.component.html
new file mode 100644 (file)
index 0000000..2158174
--- /dev/null
@@ -0,0 +1,32 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" *ngIf="isExport" i18n>Export</h4>
+    <h4 class="modal-title" *ngIf="!isExport" i18n>Import</h4>
+    <button type="button" class="close"
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <div class="row">
+      <div class="col-md-12">
+        <span *ngIf="isExport" i18n>
+          Copy this to your clipboard and save it to a file to export the settings.
+        </span>
+        <span *ngIf="!isExport" i18n>
+          Paste in your exported settings.
+        </span>
+      </div>
+    </div>
+    <div class="row mt-3">
+      <div class="col-md-12">
+        <textarea class="form-control" [(ngModel)]="jsonData" rows="4"></textarea>
+      </div>
+    </div>
+    <div class="row mt-3" *ngIf="!isExport">
+      <div class="col-md-3">
+        <button class="btn btn-outline-dark" i18n (click)="update()">Submit</button>
+      </div>
+    </div>
+  </div>
+</ng-template>
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-json-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-json-dialog.component.ts
new file mode 100644 (file)
index 0000000..f2eb3aa
--- /dev/null
@@ -0,0 +1,28 @@
+import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {OrgUnitSetting} from '@eg/staff/admin/local/org-unit-settings/org-unit-settings.component';
+
+@Component({
+  selector: 'eg-admin-ou-setting-json-dialog',
+  templateUrl: './org-unit-setting-json-dialog.component.html'
+})
+
+export class OuSettingJsonDialogComponent extends DialogComponent {
+
+    isExport: boolean;
+    @Input() jsonData: string;
+
+    constructor(
+        private modal: NgbModal
+    ) {
+        super(modal);
+    }
+
+    update() {
+        this.close({
+            apply: true,
+            jsonData: this.jsonData
+        });
+    }
+}
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings-routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings-routing.module.ts
new file mode 100644 (file)
index 0000000..c80ffdb
--- /dev/null
@@ -0,0 +1,15 @@
+import {NgModule} from '@angular/core';\r
+import {RouterModule, Routes} from '@angular/router';\r
+import {OrgUnitSettingsComponent} from './org-unit-settings.component';\r
+\r
+const routes: Routes = [{\r
+    path: '',\r
+    component: OrgUnitSettingsComponent\r
+}];\r
+\r
+@NgModule({\r
+    imports: [RouterModule.forChild(routes)],\r
+    exports: [RouterModule]\r
+})\r
+\r
+export class OrgUnitSettingsRoutingModule {}
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings.component.html
new file mode 100644 (file)
index 0000000..1da8149
--- /dev/null
@@ -0,0 +1,84 @@
+<eg-title i18n-prefix prefix="Org Unit Settings Editor"></eg-title>
+<eg-staff-banner bannerText="Org Unit Settings Config" i18n-bannerText></eg-staff-banner>
+<!-- org unit selector -->
+
+<eg-admin-edit-org-unit-setting-dialog #editOuSettingDialog>
+</eg-admin-edit-org-unit-setting-dialog>
+
+<eg-admin-ou-setting-history-dialog #orgUnitSettingHistoryDialog>
+</eg-admin-ou-setting-history-dialog>
+
+<eg-admin-ou-setting-json-dialog #ouSettingJsonDialog>
+</eg-admin-ou-setting-json-dialog>
+
+<div class="row mt-3">
+  <div class="col-md-3">
+    <div class="input-group">
+      <div class="input-group-prepend">
+        <div class="input-group-text" i18n>Context Location</div>
+        <eg-org-select [initialOrg]="contextOrg"
+          (onChange)="contextOrgChanged($event)">
+        </eg-org-select>
+      </div>
+    </div>
+  </div>
+  <div class="col-md-6">
+    <div class="input-group">
+        <input type="text"
+            class="form-control"
+            [(ngModel)]="filterString"
+            (blur)="applyFilter()"
+        />
+        <button class="btn btn-outline-dark mr-1" i18n>Filter</button>
+        <button class="btn btn-outline-dark mr-1" i18n
+          (click)="applyFilter(true)">Clear Filter</button>
+    </div>
+  </div>
+  <div class="col-md-3">
+    <div class="input-group">
+      <button class="btn btn-outline-dark mr-1"
+        (click)="showJsonDialog(true)" i18n>Export</button>
+      <button class="btn btn-outline-dark mr-1"
+        (click)="showJsonDialog(false)" i18n>Import</button>
+    </div>
+  </div>
+  
+</div>
+<!-- Org Unit Settings Grid -->
+<div class='w-11 mt-3'>
+  <eg-grid #orgUnitSettingsGrid [dataSource]="gridDataSource"
+    [disableSelect]="true"
+    [sortable]="false" [showDeclaredFieldsOnly]="true"
+    persistKey="admin.actor.org_unit_settings">
+
+    <eg-grid-column i18n-label label="Edit" name="edit"
+      [cellTemplate]="editCellTemplate"></eg-grid-column>
+    <eg-grid-column i18n-label label="History" name="history"
+      [cellTemplate]="historyCellTemplate"></eg-grid-column>
+    <eg-grid-column path="grp" label="Group" i18n-label></eg-grid-column>
+    <eg-grid-column path="label" label="Setting" [index]="true" i18n-label></eg-grid-column>
+    <eg-grid-column path="context.shortname()" label="Context" i18n-label></eg-grid-column>
+    <eg-grid-column path="value_str" label="Value" i18n-label></eg-grid-column>
+    
+  </eg-grid>
+</div>
+
+<ng-template #editCellTemplate let-entry="row">
+  <span>
+    <a
+      (click)="showEditSettingValueDialog(entry)" class="pl-1"
+      [routerLink]="" i18n>
+      Edit
+    </a>
+  </span>
+</ng-template>
+
+<ng-template #historyCellTemplate let-entry="row">
+  <span>
+    <a
+      (click)="showHistoryDialog(entry)" class="pl-1"
+      [routerLink]="" i18n>
+      History
+    </a>
+  </span>
+</ng-template>
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings.component.ts
new file mode 100644 (file)
index 0000000..9422b62
--- /dev/null
@@ -0,0 +1,368 @@
+import {Component, OnInit, Input, ViewChild, ViewEncapsulation
+    } from '@angular/core';
+import {Router} from '@angular/router';
+import {Observable, Observer, of} from 'rxjs';
+import {map} from 'rxjs/operators';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridToolbarCheckboxComponent
+    } from '@eg/share/grid/grid-toolbar-checkbox.component';
+import {StoreService} from '@eg/core/store.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+
+import {EditOuSettingDialogComponent
+    } from '@eg/staff/admin/local/org-unit-settings/edit-org-unit-setting-dialog.component';
+import {OuSettingHistoryDialogComponent
+    } from '@eg/staff/admin/local/org-unit-settings/org-unit-setting-history-dialog.component';
+import {OuSettingJsonDialogComponent
+    } from '@eg/staff/admin/local/org-unit-settings/org-unit-setting-json-dialog.component';
+
+export class OrgUnitSetting {
+    name: string;
+    label: string;
+    grp: string;
+    description: string;
+    value: any;
+    value_str: any;
+    dataType: string;
+    fmClass: string;
+    _idlOptions: IdlObject[];
+    _org_unit: IdlObject;
+    context: string;
+    view_perm: string;
+    _history: any[];
+}
+
+@Component({
+    templateUrl: './org-unit-settings.component.html'
+})
+
+export class OrgUnitSettingsComponent {
+
+    contextOrg: IdlObject;
+
+    initDone = false;
+    gridDataSource: GridDataSource;
+    gridTemplateContext: any;
+    prevFilter: string;
+    currentHistory: any[];
+    currentOptions: any[];
+    jsonFieldData: {};
+    @ViewChild('orgUnitSettingsGrid', { static:true }) orgUnitSettingsGrid: GridComponent;
+
+    @ViewChild('editOuSettingDialog', { static:true })
+        private editOuSettingDialog: EditOuSettingDialogComponent;
+    @ViewChild('orgUnitSettingHistoryDialog', { static:true })
+        private orgUnitSettingHistoryDialog: OuSettingHistoryDialogComponent;
+    @ViewChild('ouSettingJsonDialog', { static:true })
+        private ouSettingJsonDialog: OuSettingJsonDialogComponent;
+
+    refreshSettings: boolean;
+    renderFromPrefs: boolean;
+
+    settingTypeArr: any[];
+
+    @Input() filterString: string;
+
+    constructor(
+        private router: Router,
+        private org: OrgService,
+        private idl: IdlService,
+        private pcrud: PcrudService,
+        private auth: AuthService,
+        private store: ServerStoreService,
+        private localStore: StoreService,
+        private toast: ToastService,
+        private net: NetService,
+    ) {
+        this.gridDataSource = new GridDataSource();
+        this.refreshSettings = true;
+        this.renderFromPrefs = true;
+
+        this.contextOrg = this.org.get(this.auth.user().ws_ou());
+    }
+
+    ngOnInit() {
+        this.initDone = true;
+        this.settingTypeArr = [];
+        this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+            return this.fetchSettingTypes(pager);
+        };
+    }
+
+    fetchSettingTypes(pager: Pager): Observable<any> {
+        return new Observable<any>(observer => {
+            this.pcrud.retrieveAll('coust', {flesh: 3, flesh_fields: {
+                'coust': ['grp', 'view_perm']
+            }},
+            { authoritative: true }).subscribe(
+                settingTypes => this.allocateSettingTypes(settingTypes),
+                err => {},
+                ()  => {
+                    this.refreshSettings = false;
+                    this.mergeSettingValues().then(
+                        ok => {
+                            this.flattenSettings(observer);
+                        }
+                    );
+                }
+            );
+        });
+    }
+
+    mergeSettingValues(): Promise<any> {
+        const settingNames = this.settingTypeArr.map(setting => setting.name);
+        return new Promise((resolve, reject) => {
+            this.net.request(
+                'open-ils.actor',
+                'open-ils.actor.ou_setting.ancestor_default.batch',
+                 this.contextOrg.id(), settingNames, this.auth.token()
+            ).subscribe(
+                blob => {
+                    let settingVals = Object.keys(blob).map(key => {
+                        return {'name': key, 'setting': blob[key]}
+                    });
+                    settingVals.forEach(key => {
+                        if (key.setting) {
+                            let settingsObj = this.settingTypeArr.filter(
+                                setting => setting.name == key.name
+                            )[0];
+                            settingsObj.value = key.setting.value;
+                            settingsObj.value_str = settingsObj.value;
+                            if (settingsObj.dataType == 'link' && (key.setting.value || key.setting.value == 0)) {
+                                this.fetchLinkedField(settingsObj.fmClass, key.setting.value, settingsObj.value_str).then(res => {
+                                    settingsObj.value_str = res;
+                                });
+                            }
+                            settingsObj._org_unit = this.org.get(key.setting.org);
+                            settingsObj.context = settingsObj._org_unit.shortname();
+                        }
+                    });
+                    resolve(this.settingTypeArr);
+                },
+                err => reject(err)
+            );
+        });
+    }
+
+    fetchLinkedField(fmClass, id, val) {
+        return new Promise((resolve, reject) => {
+            return this.pcrud.retrieve(fmClass, id).subscribe(linkedField => {
+                val = linkedField.name();
+                resolve(val);
+            });
+        });
+    }
+
+    fetchHistory(setting): Promise<any> {
+        let name = setting.name;
+        return new Promise((resolve, reject) => {
+            this.net.request(
+                'open-ils.actor',
+                'open-ils.actor.org_unit.settings.history.retrieve',
+                this.auth.token(), name, this.contextOrg.id()
+            ).subscribe(res=> {
+                this.currentHistory = [];
+                if (!Array.isArray(res)) {
+                    res = [res];
+                }
+                res.forEach(log => {
+                    log.org = this.org.get(log.org);
+                    log.new_value_str = log.new_value;
+                    log.original_value_str = log.original_value;
+                    if (setting.dataType == "link") {
+                        if (log.new_value) {
+                            this.fetchLinkedField(setting.fmClass, parseInt(log.new_value), log.new_value_str).then(val => {
+                                log.new_value_str = val;
+                            });
+                        }
+                        if (log.original_value) {
+                            this.fetchLinkedField(setting.fmClass, parseInt(log.original_value), log.original_value_str).then(val => {
+                                log.original_value_str = val;
+                            });
+                        }
+                    }
+                    if (log.new_value_str) log.new_value_str = log.new_value_str.replace(/^"(.*)"$/, '$1');
+                    if (log.original_value_str) log.original_value_str = log.original_value_str.replace(/^"(.*)"$/, '$1');
+                });
+                this.currentHistory = res;
+                this.currentHistory.sort((a, b) => {
+                    return a.date_applied < b.date_applied ? 1 : -1;
+                });
+
+                resolve(this.currentHistory);
+            }, err=>{reject(err);});
+        });
+    }
+
+    allocateSettingTypes(coust: IdlObject) {
+        let entry = new OrgUnitSetting();
+        entry.name = coust.name();
+        entry.label = coust.label();
+        entry.dataType = coust.datatype();
+        if (coust.fm_class()) entry.fmClass = coust.fm_class();
+        if (coust.description()) entry.description = coust.description();
+        // For some reason some setting types don't have a grp, should look into this...
+        if (coust.grp()) entry.grp = coust.grp().label();
+        if (coust.view_perm()) 
+            entry.view_perm = coust.view_perm().code();
+
+        this.settingTypeArr.push(entry);
+    }
+
+    flattenSettings(observer: Observer<any>) {
+        this.gridDataSource.data = this.settingTypeArr;
+        observer.complete();
+    }
+
+    contextOrgChanged(org: IdlObject) {
+        this.updateGrid(org);
+    }
+
+    applyFilter(clear?: boolean) {
+        if (clear) this.filterString = '';
+        this.updateGrid(this.contextOrg);
+    }
+    
+    updateSetting(obj, entry) {
+        this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.org_unit.settings.update',
+            this.auth.token(), obj.context.id(), obj.setting
+        ).toPromise().then(res=> {
+            this.toast.success(entry.label + " Updated.");
+            if (!obj.setting[entry.name]) {
+                let settingsObj = this.settingTypeArr.filter(
+                    setting => setting.name == entry.name
+                )[0];
+                settingsObj.value = null;
+                settingsObj._org_unit = null;
+                settingsObj.context = null;
+            }
+            this.mergeSettingValues();
+        },
+        err => {
+            this.toast.danger(entry.label + " failed to update: " + err.desc);
+        });
+    }
+
+    showEditSettingValueDialog(entry: OrgUnitSetting) {
+        this.editOuSettingDialog.entry = entry;
+        this.editOuSettingDialog.entryValue = entry.value;
+        this.editOuSettingDialog.entryContext = entry._org_unit || this.contextOrg;
+        this.editOuSettingDialog.open({size: 'lg'}).subscribe(
+            res => {
+                this.updateSetting(res, entry);
+            }
+        );
+    }
+
+    showHistoryDialog(entry: OrgUnitSetting) {
+        if (entry) {
+            this.fetchHistory(entry).then(
+                fetched => {
+                    this.orgUnitSettingHistoryDialog.history = this.currentHistory;
+                    this.orgUnitSettingHistoryDialog.gridDataSource.data = this.currentHistory;
+                    this.orgUnitSettingHistoryDialog.entry = entry;
+                    this.orgUnitSettingHistoryDialog.open({size: 'lg'}).subscribe(res => {
+                        if (res.revert) {
+                            this.updateSetting(res, entry);
+                        }
+                    });
+                }
+            )
+        }
+    }
+
+    showJsonDialog(isExport: boolean) {
+        this.ouSettingJsonDialog.isExport = isExport;
+        this.ouSettingJsonDialog.jsonData = "";
+        if (isExport) {
+            this.ouSettingJsonDialog.jsonData = "{";
+            this.gridDataSource.data.forEach(entry => {
+                this.ouSettingJsonDialog.jsonData +=
+                    "\"" + entry.name + "\": {\"org\": \"" +
+                    this.contextOrg.id() + "\", \"value\": ";
+                if (entry.value) {
+                    this.ouSettingJsonDialog.jsonData += "\"" + entry.value + "\"";
+                } else {
+                    this.ouSettingJsonDialog.jsonData += "null";
+                }
+                this.ouSettingJsonDialog.jsonData += "}";
+                if (this.gridDataSource.data.indexOf(entry) != (this.gridDataSource.data.length - 1))
+                    this.ouSettingJsonDialog.jsonData += ",";
+            });
+            this.ouSettingJsonDialog.jsonData += "}";
+        }
+
+        this.ouSettingJsonDialog.open({size: 'lg'}).subscribe(res => {
+            if (res.apply && res.jsonData) {
+                let jsonSettings = JSON.parse(res.jsonData);
+                Object.entries(jsonSettings).forEach((fields) => {
+                    let entry = this.settingTypeArr.find(x => x.name == fields[0]);
+                    let obj = {setting: {}, context: {}};
+                    let val = this.parseValType(fields[1]['value'], entry.dataType);
+                    obj.setting[fields[0]] = val;
+                    obj.context = this.org.get(fields[1]['org']);
+                    this.updateSetting(obj, entry);
+                });
+            }
+        });
+    }
+
+    parseValType(value, dataType) {
+        if (dataType == "integer" || "currency" || "link") {
+            return Number(value);
+        } else if (dataType == "bool") {
+            return (value === 'true');
+        } else {
+            return value;
+        }
+    }
+    
+    filterCoust() {
+        if (this.filterString != this.prevFilter) {
+            this.prevFilter = this.filterString;
+            if (this.filterString) {
+                this.gridDataSource.data = [];
+                let tempGrid = this.settingTypeArr;
+                tempGrid.forEach(row => {
+                    let containsString =
+                         row.name.includes(this.filterString) ||
+                         row.label.includes(this.filterString) ||
+                         (row.grp && row.grp.includes(this.filterString)) ||
+                         (row.description && row.description.includes(this.filterString));
+                    if (containsString) {
+                        this.gridDataSource.data.push(row);
+                    }
+                });
+            } else {
+                this.gridDataSource.data = this.settingTypeArr;
+            }
+        }
+    }
+
+    updateGrid(org) {
+        if (this.contextOrg != org) {
+            this.contextOrg = org;
+            this.refreshSettings = true;
+        }
+
+        if (this.filterString != this.prevFilter) {
+            this.refreshSettings = true;
+        }
+
+        if (this.refreshSettings) { 
+            this.mergeSettingValues().then(
+                res => this.filterCoust()
+            );
+        }
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings.module.ts
new file mode 100644 (file)
index 0000000..62ee75e
--- /dev/null
@@ -0,0 +1,29 @@
+import {NgModule} from '@angular/core';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {TreeModule} from '@eg/share/tree/tree.module';
+import {OrgUnitSettingsComponent} from './org-unit-settings.component';
+import {EditOuSettingDialogComponent} from './edit-org-unit-setting-dialog.component';
+import {OuSettingHistoryDialogComponent} from './org-unit-setting-history-dialog.component';
+import {OrgUnitSettingsRoutingModule} from './org-unit-settings-routing.module';
+import {OuSettingJsonDialogComponent} from './org-unit-setting-json-dialog.component';
+
+@NgModule({
+    declarations: [
+        OrgUnitSettingsComponent,
+        EditOuSettingDialogComponent,
+        OuSettingHistoryDialogComponent,
+        OuSettingJsonDialogComponent
+    ],
+    imports: [
+        AdminCommonModule,
+        OrgUnitSettingsRoutingModule,
+        TreeModule
+    ],
+    exports: [
+    ],
+    providers: [
+    ]
+})
+
+export class OrgUnitSettingsModule {
+}
\ No newline at end of file
index 2376260..fd9e5e8 100644 (file)
@@ -69,7 +69,10 @@ const routes: Routes = [{
     }]
 }, {
     path: 'config/standing_penalty',
-    component: StandingPenaltyComponent
+    component: StandingPenaltyComponent,
+}, {
+    path: 'asset/org_unit_settings',
+    loadChildren: '@eg/staff/admin/local/org-unit-settings/org-unit-settings.module#OrgUnitSettingsModule'
 }, {
     path: 'config/ui_staff_portal_page_entry',
     component: AdminStaffPortalPageComponent
index 3adc2a3..01741b5 100644 (file)
@@ -186,6 +186,33 @@ sub update_privacy_waiver {
     return 1;
 }
 
+__PACKAGE__->register_method(
+    method    => "get_ou_setting_history",
+    api_name  => "open-ils.actor.org_unit.settings.history.retrieve",
+    signature => {
+        desc => "Retrieves the history of an Org Unit Setting.  The permission to retrieve "          .
+                "an org unit setting's history is dependant on a specific permission specified "       .
+                "in the view_perm column of the config.org_unit_setting_type " .
+                "table's row corresponding to the setting being changed." ,
+        params => [
+            {desc => 'Authentication token',        type => 'string'},
+            {desc => 'Org Unit ID',                 type => 'number'},
+            {desc => 'Setting Type Name',           type => 'string'}
+        ],
+        return => {desc => 'History IDL Object'}
+    }
+);
+
+sub get_ou_setting_history {
+    my( $self, $client, $auth, $setting, $orgid ) = @_;
+    my $e = new_editor(authtoken => $auth, xact => 1);
+    return $e->die_event unless $e->checkauth;
+
+    return $U->ou_ancestor_setting_log(
+        $orgid, $setting, $e, $auth
+    );
+
+}
 
 __PACKAGE__->register_method(
     method    => "set_ou_settings",
index 566342b..3c32377 100644 (file)
@@ -1318,19 +1318,63 @@ sub ou_ancestor_setting {
         my $coust = $e->retrieve_config_org_unit_setting_type([
             $name, {flesh => 1, flesh_fields => {coust => ['view_perm']}}
         ]);
-        if ($coust && $coust->view_perm) {
-            # And you can't have permission if you don't have a valid session.
-            return undef if not $e->checkauth;
-            # And now that we know you MIGHT have permission, we check it.
-            return undef if not $e->allowed($coust->view_perm->code, $orgid);
-        }
+        return undef unless ou_ancestor_setting_perm_check($orgid, $coust, $auth)
     }
 
     my $query = {from => ['actor.org_unit_ancestor_setting', $name, $orgid]};
     my $setting = $e->json_query($query)->[0];
     return undef unless $setting;
     return {org => $setting->{org_unit}, value => OpenSRF::Utils::JSON->JSON2perl($setting->{value})};
-}   
+}
+
+# Returns the org id if the requestor has the permissions required
+# to view the ou setting.
+sub ou_ancestor_setting_perm_check {
+    my( $self, $orgid, $view_perm, $e, $auth ) = @_;
+    $e = $e || OpenILS::Utils::CStoreEditor->new(
+        (defined $auth) ? (authtoken => $auth) : ()
+    );
+
+    # And you can't have permission if you don't have a valid session.
+    return undef if not $e->checkauth;
+    # And now that we know you MIGHT have permission, we check it.
+    if ($view_perm) {
+        return undef unless $e->allowed($view_perm, $orgid);
+    }
+
+    return $orgid;
+}
+
+sub ou_ancestor_setting_log {
+    my ( $self, $orgid, $name, $e, $auth ) = @_;
+    $e = $e || OpenILS::Utils::CStoreEditor->new(
+        (defined $auth) ? (authtoken => $auth, xact => 1) : ()
+    );
+    my $coust;
+
+    if ($auth) {
+        $coust = $e->retrieve_config_org_unit_setting_type([
+            $name, {flesh => 1, flesh_fields => {coust => ['view_perm']}}
+        ]);
+        my $orgs = $self->get_org_ancestors($orgid);
+
+        my $qorg = $self->ou_ancestor_setting_perm_check(
+            $orgs,
+            $coust,
+            $e,
+            $auth
+        );
+        my $sort = { order_by => { coustl => 'date_applied DESC' } };
+        return $e->json_query({
+            from => 'coustl',
+            where => {
+                field_name => $name,
+                org => $qorg
+            },
+            $sort
+        });
+    };
+}
 
 # This fetches a set of OU settings in one fell swoop,
 # which can be significantly faster than invoking
index e662764..b74a461 100644 (file)
@@ -19534,6 +19534,12 @@ VALUES (
         'cwst', 'label'
     )
 ), (
+    'eg.grid.asset.ouSettings', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.asset.ouSettings',
+        'Grid Config: asset.ouSettings',
+        'cwst', 'label'
+), (
     'eg.cat.record.summary.collapse', 'gui', 'bool',
     oils_i18n_gettext(
         'eg.cat.record.summary.collapse',
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.ouSettings-grid-ws-settings.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.ouSettings-grid-ws-settings.sql
new file mode 100644 (file)
index 0000000..92141f4
--- /dev/null
@@ -0,0 +1,15 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label) 
+VALUES (
+    'eg.grid.asset.ouSettings', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.asset.ouSettings',
+        'Grid Config: asset.ouSettings',
+        'cwst', 'label'
+    )
+);
+
+COMMIT;
\ No newline at end of file