LP#1832897: add administrative interfaces for carousels
authorGalen Charlton <gmc@equinoxinitiative.org>
Sun, 9 Jun 2019 23:18:46 +0000 (19:18 -0400)
committerJane Sandberg <sandbej@linnbenton.edu>
Wed, 4 Sep 2019 02:31:55 +0000 (19:31 -0700)
This patch adds three Angular administration interfaces:

(Server Admin) Carousel Types
(Local Admin) Carousel Library Mappings
(Local Admin) Carousels

Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
Open-ILS/src/eg2/src/app/staff/admin/local/admin-carousel.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/admin-carousel.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/admin-local.module.ts
Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts
Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html

diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/admin-carousel.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/admin-carousel.component.html
new file mode 100644 (file)
index 0000000..6c3e783
--- /dev/null
@@ -0,0 +1,98 @@
+<eg-title i18n-prefix prefix="{{classLabel}} Administration">
+</eg-title>
+<eg-staff-banner bannerText="{{classLabel}} Configuration" i18n-bannerText>
+</eg-staff-banner>
+
+<ng-template #successStrTmpl i18n>{{idlClassDef.label}} Update Succeeded</ng-template>
+<eg-string #successString [template]="successStrTmpl"></eg-string>
+
+<ng-template #updateFailedStrTmpl i18n>Update of {{idlClassDef.label}} failed</ng-template>
+<eg-string #updateFailedString [template]="updateFailedStrTmpl"></eg-string>
+
+<ng-template #deleteFailedStrTmpl i18n>Delete of carousel faiiled or was not allowed</ng-template>
+<eg-string #deleteFailedString [template]="deleteFailedStrTmpl"></eg-string>
+
+<ng-template #deleteSuccessStrTmpl i18n>Carousel Successfully Deleted</ng-template>
+<eg-string #deleteSuccessString [template]="deleteSuccessStrTmpl"></eg-string>
+
+<ng-template #createStrTmpl i18n>{{idlClassDef.label}} Succeessfully Created</ng-template>
+<eg-string #createString [template]="createStrTmpl"></eg-string>
+
+<ng-template #createErrStrTmpl i18n>Failed to create new {{idlClassDef.label}}</ng-template>
+<eg-string #createErrString [template]="createErrStrTmpl"></eg-string>
+
+<ng-template #refreshStrTmpl i18n let-name="name">{{name}} is being refreshed. It may take a couple minutes.</ng-template>
+<eg-string #refreshString [template]="refreshStrTmpl"></eg-string>
+
+<ng-template #refreshErrStrTmpl i18n let-name="name">{{name}} is manual and cannot be refreshed automatically.</ng-template>
+<eg-string #refreshErrString [template]="refreshErrStrTmpl"></eg-string>
+
+<ng-container *ngIf="orgField">
+  <eg-org-family-select
+    [limitPerms]="viewPerms"
+    [selectedOrgId]="contextOrg.id()"
+    [(ngModel)]="searchOrgs"
+    (ngModelChange)="grid.reload()">
+  </eg-org-family-select>
+  <hr/>
+</ng-container>
+
+<!-- idlObject and fieldName applied programmatically -->
+<eg-translate #translator></eg-translate>
+
+<eg-grid #grid idlClass="{{idlClass}}" [dataSource]="dataSource" 
+    [sortable]="true" persistKey="{{persistKey}}" [showLinkSelectors]="true">
+  <eg-grid-toolbar-button [disabled]="!canCreate" 
+    label="New {{idlClassDef.label}}" i18n-label [action]="createNew">
+  </eg-grid-toolbar-button>
+  <eg-grid-toolbar-button [disabled]="translatableFields.length == 0" 
+    label="Apply Translations" i18n-label [action]="translate">
+  </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 [action]="deleteSelected">
+  </eg-grid-toolbar-action>
+  <eg-grid-toolbar-action label="Refesh Selected" i18n-label [action]="refreshSelected">
+  </eg-grid-toolbar-action>
+  <eg-grid-column path="bucket" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="creator" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="editor" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="create_time" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="edit_time" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="age_filter" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="owning_lib_filter" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="copy_location_filter" [hidden]="true"></eg-grid-column>
+</eg-grid>
+
+<ng-template #bucketTemplate
+    let-field="field" let-record="record">
+  <span *ngIf="record[field.name]()" i18n>
+    <a href="/eg/staff/cat/bucket/record/view/{{record[field.name]()}}" target="_blank" i18n>Link to bucket</a>
+    <span *ngIf="record['type']() !== 1" i18n style="font-style: italic"> (Note: changes to bucket contents may be overwritten by the next carousel update.)</span>
+  </span>
+</ng-template>
+
+<ng-template #locationTemplate
+    let-field="field" let-record="record">
+  <eg-multi-select idlClass="acpl" linkedLibraryLabel="owning_lib"
+                   [startValue]="record['copy_location_filter']()"
+                   (onChange)="record['copy_location_filter']($event)">
+  </eg-multi-select>
+</ng-template>
+<ng-template #orgTemplate
+    let-field="field" let-record="record">
+  <eg-multi-select idlClass="aou"
+                   [startValue]="record['owning_lib_filter']()"
+                   (onChange)="record['owning_lib_filter']($event)">
+  </eg-multi-select>
+</ng-template>
+
+<eg-fm-record-editor #editDialog idlClass="{{idlClass}}" 
+    [preloadLinkedValues]="true" readonlyFields="last_refresh_time"
+    hiddenFieldsList="creator,editor,create_time,edit_time"
+    [preSave]="mungeCarousel" (onSave$)="postSave($event)"
+    readonlyFields="last_refresh_time"
+    [fieldOptions]="{bucket:{customTemplate:{template:bucketTemplate}},copy_location_filter:{customTemplate:{template:locationTemplate}},owning_lib_filter:{customTemplate:{template:orgTemplate}}}"
+></eg-fm-record-editor>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/admin-carousel.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/admin-carousel.component.ts
new file mode 100644 (file)
index 0000000..bfbd420
--- /dev/null
@@ -0,0 +1,130 @@
+import {Component, Input, ViewChild, OnInit} from '@angular/core';
+import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.component';
+import {ActivatedRoute} from '@angular/router';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {PermService} from '@eg/core/perm.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {StringComponent} from '@eg/share/string/string.component';
+
+@Component({
+    templateUrl: './admin-carousel.component.html'
+})
+
+export class AdminCarouselComponent extends AdminPageComponent implements OnInit {
+
+    idlClass = 'cc';
+    classLabel: string;
+
+    refreshSelected: (idlThings: IdlObject[]) => void;
+    createNew: () => void;
+    deleteSelected: (idlThings: IdlObject[]) => void;
+
+    @ViewChild('refreshString') refreshString: StringComponent;
+    @ViewChild('refreshErrString') refreshErrString: StringComponent;
+
+    constructor(
+        route: ActivatedRoute,
+        idl: IdlService,
+        org: OrgService,
+        auth: AuthService,
+        pcrud: PcrudService,
+        perm: PermService,
+        toast: ToastService,
+        private net: NetService
+    ) {
+        super(route, idl, org, auth, pcrud, perm, toast);
+    }
+
+    ngOnInit() {
+        super.ngOnInit();
+
+        this.classLabel = this.idlClassDef.label;
+        this.includeOrgDescendants = true;
+
+
+        this.createNew = () => {
+            super.createNew();
+        };
+
+        this.deleteSelected = (idlThings: IdlObject[]) => {
+            super.deleteSelected(idlThings);
+        };
+
+        this.refreshSelected = (idlThings: IdlObject[]) =>  {
+            idlThings.forEach(cc => {
+                if (cc.type().automatic() === 't') {
+                    this.net.request(
+                        'open-ils.actor',
+                        'open-ils.actor.carousel.refresh',
+                        this.auth.token(), cc.id()
+                    ).toPromise(); // fire and forget, as this could take a couple minutes
+                    this.refreshString.current({ name: cc.name() }).then(str => this.toast.success(str));
+                } else {
+                    this.refreshErrString.current({ name: cc.name() }).then(str => this.toast.warning(str));
+                }
+            });
+        };
+    }
+
+    mungeCarousel(editMode: string, rec: IdlObject) {
+        if (editMode === 'create') {
+            rec.creator(this.auth.user().id());
+        }
+        rec.editor(this.auth.user().id());
+        rec.edit_time('now');
+
+        // convert empty string to nulls as needed
+        // for int[] columns
+        if (rec.owning_lib_filter() === '') {
+            rec.owning_lib_filter(null);
+        }
+        if (rec.copy_location_filter() === '') {
+            rec.copy_location_filter(null);
+        }
+    }
+
+    postSave(rec: IdlObject) {
+        if (rec._isfieldmapper) {
+            // if we got an actual IdlObject back, the
+            // record had just been created, not just
+            // edited. therefore, we probably need
+            if (rec.bucket() == null) {
+                const bucket = this.idl.create('cbreb');
+                bucket.owner(this.auth.user().id());
+                bucket.name('System-generated bucket for carousel: ' + rec.id()); // FIXME I18N
+                bucket.btype('carousel');
+                bucket.pub('t');
+                bucket.owning_lib(rec.owner());
+                rec.bucket(bucket);
+                this.net.request(
+                    'open-ils.actor',
+                    'open-ils.actor.container.create',
+                    this.auth.token(), 'biblio', bucket
+                ).toPromise().then(
+                    newBucket => {
+                        const ccou = this.idl.create('ccou');
+                        ccou.carousel(rec.id());
+                        ccou.org_unit(rec.owner());
+                        ccou.seq(0);
+                        rec.bucket(newBucket);
+                        this.pcrud.create(ccou).subscribe(
+                            ok => {
+                                this.pcrud.update(rec).subscribe(
+                                    ok2 => console.debug('updated'),
+                                    err => console.error(err),
+                                    () => { this.grid.reload(); }
+                                );
+                            },
+                            err => console.error(err),
+                            () => { this.grid.reload(); }
+                        );
+                    }
+                );
+            }
+        }
+    }
+}
index edbb68f..2ec554d 100644 (file)
       url="/eg/staff/admin/local/config/auto_print"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Barcode Completion" 
       routerLink="/staff/admin/local/config/barcode_completion"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Carousel Library Mappings"
+      routerLink="/staff/admin/server/container/carousel_org_unit"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Carousels"
+      routerLink="/staff/admin/server/container/carousel"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Cash Reports" 
       url="/eg/staff/admin/local/money/cash_reports"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Circulation Limit Sets" 
index c2b5041..7c8a6fa 100644 (file)
@@ -5,11 +5,13 @@ import {AdminLocalRoutingModule} from './routing.module';
 import {AdminCommonModule} from '@eg/staff/admin/common.module';
 import {AdminLocalSplashComponent} from './admin-local-splash.component';
 import {AddressAlertComponent} from './address-alert.component';
+import {AdminCarouselComponent} from './admin-carousel.component';
 
 @NgModule({
   declarations: [
       AdminLocalSplashComponent,
-      AddressAlertComponent
+      AddressAlertComponent,
+      AdminCarouselComponent
   ],
   imports: [
     AdminCommonModule,
index 12030dd..4eda585 100644 (file)
@@ -3,6 +3,7 @@ import {RouterModule, Routes} from '@angular/router';
 import {AdminLocalSplashComponent} from './admin-local-splash.component';
 import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.component';
 import {AddressAlertComponent} from './address-alert.component';
+import {AdminCarouselComponent} from './admin-carousel.component';
 
 const routes: Routes = [{
     path: 'splash',
@@ -15,6 +16,9 @@ const routes: Routes = [{
     path: 'actor/address_alert',
     component: AddressAlertComponent
 }, {
+    path: 'container/carousel',
+    component: AdminCarouselComponent
+}, {
     path: ':schema/:table',
     component: BasicAdminPageComponent
 }];
index f71dd2f..d3ed6eb 100644 (file)
@@ -25,6 +25,8 @@
       routerLink="/staff/admin/server/asset/call_number_prefix"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Call Number Suffixes"  
       routerLink="/staff/admin/server/asset/call_number_suffix"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Carousel Types"
+      routerLink="/staff/admin/server/config/carousel_type"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Circulation Duration Rules"  
       routerLink="/staff/admin/server/config/rule_circ_duration"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Circulation Limit Groups"