LP1879335 Manage Authorities Angular Port
authorBill Erickson <berickxx@gmail.com>
Fri, 15 May 2020 19:11:06 +0000 (15:11 -0400)
committerGalen Charlton <gmc@equinoxinitiative.org>
Fri, 11 Sep 2020 13:58:45 +0000 (09:58 -0400)
Migrates the Manage Authorities interface from Dojo to Angular.  In
addition to the existing functionality, the UI contains additional
authority record data and it's now possible to jump to a list of linked
bib records.

Includes release notes.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
19 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/eg2/src/app/staff/cat/authority/authority.module.ts
Open-ILS/src/eg2/src/app/staff/cat/authority/browse.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/authority/browse.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/authority/browse.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/authority/manage.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/authority/manage.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/authority/merge-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/authority/merge-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/authority/routing.module.ts
Open-ILS/src/eg2/src/app/staff/nav.component.html
Open-ILS/src/eg2/src/app/staff/share/bib-list/bib-list.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/bib-list/bib-list.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/bib-list/bib-list.module.ts [new file with mode: 0644]
Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Authority.pm
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.manage-authority-grids.sql [new file with mode: 0644]
Open-ILS/src/templates/staff/navbar.tt2
docs/RELEASE_NOTES_NEXT/Cataloging/manage-auths-ang.adoc [new file with mode: 0644]

index 520f6e6..60e4bea 100644 (file)
@@ -2772,6 +2772,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field name="record" reporter:datatype="link" />
                        <field name="atag" reporter:datatype="link" />
                        <field name="value" reporter:datatype="text" />
+                       <field name="thesaurus" reporter:datatype="text" />
                </fields>
                <links>
                        <link field="record" reltype="has_a" key="id" map="" class="are"/>
index ded954a..f29b20c 100644 (file)
@@ -4,18 +4,28 @@ import {CommonWidgetsModule} from '@eg/share/common-widgets.module';
 import {AuthorityRoutingModule} from './routing.module';
 import {MarcEditModule} from '@eg/staff/share/marc-edit/marc-edit.module';
 import {AuthorityMarcEditComponent} from './marc-edit.component';
+import {BrowseAuthorityComponent} from './browse.component';
+import {ManageAuthorityComponent} from './manage.component';
+import {AuthorityMergeDialogComponent} from './merge-dialog.component';
+import {BrowseService} from './browse.service';
+import {BibListModule} from '@eg/staff/share/bib-list/bib-list.module';
 
 @NgModule({
   declarations: [
-    AuthorityMarcEditComponent
+    AuthorityMarcEditComponent,
+    BrowseAuthorityComponent,
+    ManageAuthorityComponent,
+    AuthorityMergeDialogComponent
   ],
   imports: [
     StaffCommonModule,
     CommonWidgetsModule,
     MarcEditModule,
-    AuthorityRoutingModule
+    AuthorityRoutingModule,
+    BibListModule
   ],
   providers: [
+    BrowseService
   ]
 })
 
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/authority/browse.component.html b/Open-ILS/src/eg2/src/app/staff/cat/authority/browse.component.html
new file mode 100644 (file)
index 0000000..94a0f53
--- /dev/null
@@ -0,0 +1,94 @@
+<eg-staff-banner bannerText="Manage Authority Records" i18n-bannerText>
+</eg-staff-banner>
+
+<eg-string #rowSelected text="Row Selected for Merge" i18n-text></eg-string>
+
+<eg-authority-merge-dialog #mergeDialog></eg-authority-merge-dialog>
+
+<div class="row form-inline mb-3">
+  <div class="col-lg-3">
+    <div class="input-group">
+      <div class="input-group-prepend">
+        <span class="input-group-text" id="search-term" i18n>Search Term</span>
+      </div>
+      <input type="text" class="form-control" placeholder="Search Term" 
+        i18n-placeholder aria-describedby="search-term" 
+        (change)="search(null, true)"
+        (keyup.enter)="search(null, true)" [(ngModel)]="browse.searchTerm">
+    </div>
+  </div>
+  <div class="col-lg-5">
+    <div class="input-group">
+      <div class="input-group-prepend">
+        <span class="input-group-text" id="auth-axis" i18n>Authority Type</span>
+      </div>
+      <eg-combobox #axisCbox [(ngModel)]="authorityAxis" 
+        [entries]="browse.authorityAxes" (onChange)="search(null, true)">
+      </eg-combobox>
+      <!--
+      Hiding 'submit' button since it should never be necessary, plus it
+      can lead to firing duplicate searches if you're quick on the draw.
+      If we want it back, uncomment and add a [disabled] attribute to
+      prevent dupe searches.
+      <button class="btn btn-outline-dark ml-2" (click)="search()" i18n>Submit</button>
+      -->
+    </div>
+  </div>
+  <div class="col-lg-4 d-flex">
+    <div class="flex-1"></div><!-- push right -->
+    <div class="form-inline">
+      <button class="btn btn-outline-dark ml-2" (click)="search(-1)" i18n>Previous</button>
+      <label for='offset-input' class="form-control ml-2" i18n>Page</label>
+      <input class="form-control" type="number" 
+        [(ngModel)]="browse.searchOffset" id="offset-input" (change)="search()"/>
+      <button class="btn btn-outline-dark ml-2" (click)="search(1)" i18n>Next</button>
+    </div>
+  </div>
+</div>
+
+<ng-template #headingTemplate let-row="row">
+  <a routerLink="/staff/cat/authority/manage/{{row.authority.id()}}/bibs"
+    i18n-title title="Manage Authority {{row.authority.id()}}">
+    {{row.heading}}
+  </a>
+</ng-template>
+
+<eg-grid #grid [dataSource]="dataSource" [disablePaging]="true"
+  [rowFlairIsEnabled]="true" [rowFlairCallback]="rowFlairCallback"
+  [cellTextGenerator]="cellTextGenerator" persistKey="cat.authority.browse">
+
+  <eg-grid-toolbar-action label="Mark for Merge" i18n-label
+    (onClick)="markForMerge($event)"></eg-grid-toolbar-action>
+
+  <eg-grid-toolbar-action label="Un-Mark for Merge" i18n-label
+    (onClick)="unMarkForMerge($event)"></eg-grid-toolbar-action>
+
+  <eg-grid-toolbar-action label="Clear All Merge Marks" i18n-label
+    (onClick)="clearMergeSelection()"></eg-grid-toolbar-action>
+
+  <eg-grid-toolbar-action label="Merge Marked Records" i18n-label
+    (onClick)="openMergeDialog()"></eg-grid-toolbar-action>
+
+  <eg-grid-column name="id" label="ID" path="authority.id" i18n-label 
+    [index]="true" flex="1"></eg-grid-column>
+  <eg-grid-column name="link_count" label="Linked Bibs" 
+    i18n-label flex="1"></eg-grid-column>
+  <eg-grid-column name="heading" label="Heading" i18n-label flex="3"
+    [cellTemplate]="headingTemplate"></eg-grid-column>
+  <eg-grid-column name="control_set" path="authority.control_set.name" 
+    label="Control Set" i18n-label flex="1"></eg-grid-column>
+  <eg-grid-column name="thesaurus" label="Thesaurus" i18n-label flex="1"></eg-grid-column>
+  <eg-grid-column name="thesaurus_code" label="Thesaurus Code" 
+    i18n-label flex="1"></eg-grid-column>
+  <eg-grid-column name="creator" label="Creator" i18n-label
+    path="authority.creator.usrname" flex="1"></eg-grid-column>
+  <eg-grid-column name="create_date" label="Create Date" i18n-label
+    path="authority.create_date" flex="1" datatype="timestamp"></eg-grid-column>
+  <eg-grid-column name="edit_date" label="Edit Date" i18n-label
+    path="authority.edit_date" flex="1" datatype="timestamp"></eg-grid-column>
+  <eg-grid-column name="source" label="Source" i18n-label
+    path="authority.source" flex="1"></eg-grid-column>
+  <eg-grid-column name="owner" label="Owner" i18n-label
+    path="authority.owner" flex="1"></eg-grid-column>
+</eg-grid>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/authority/browse.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/authority/browse.component.ts
new file mode 100644 (file)
index 0000000..8f6dc1e
--- /dev/null
@@ -0,0 +1,123 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Observable, empty} from 'rxjs';
+import {map, switchMap} from 'rxjs/operators';
+import {IdlObject} from '@eg/core/idl.service';
+import {Pager} from '@eg/share/util/pager';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridContext, GridDataSource, GridCellTextGenerator,
+    GridRowFlairEntry} from '@eg/share/grid/grid';
+import {ComboboxEntry, ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {BrowseService} from './browse.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {AuthorityMergeDialogComponent} from './merge-dialog.component';
+
+/* Find, merge, and edit authority records */
+
+@Component({
+  templateUrl: 'browse.component.html',
+  styles: [`#offset-input { width: 4em; }`]
+})
+export class BrowseAuthorityComponent implements OnInit {
+
+    authorityAxis: ComboboxEntry;
+    dataSource: GridDataSource;
+    cellTextGenerator: GridCellTextGenerator;
+
+    rowFlairCallback: (row: any) => GridRowFlairEntry;
+
+    @ViewChild('grid', {static: false}) grid: GridComponent;
+    @ViewChild('axisCbox', {static: false}) axisCbox: ComboboxComponent;
+    @ViewChild('rowSelected', {static: false}) rowSelected: StringComponent;
+    @ViewChild('mergeDialog', {static: false})
+        mergeDialog: AuthorityMergeDialogComponent;
+
+    constructor(
+        private net: NetService,
+        private org: OrgService,
+        private pcrud: PcrudService,
+        public browse: BrowseService
+    ) {}
+
+    ngOnInit() {
+        this.browse.fetchAxes();
+        this.setupGrid();
+    }
+
+    setupGrid() {
+        this.dataSource = new GridDataSource();
+
+        this.dataSource.getRows =
+            (pager: Pager, sort: any): Observable<any> => {
+
+            if (this.authorityAxis) {
+                this.browse.authorityAxis = this.authorityAxis.id;
+
+            } else {
+                // Our browse service may have cached search params
+                if (this.browse.authorityAxis) {
+                    this.axisCbox.selectedId = this.browse.authorityAxis;
+                    this.authorityAxis = this.axisCbox.selected;
+                } else {
+                    return empty();
+                }
+            }
+
+            return this.browse.loadAuthorities();
+        };
+
+        this.cellTextGenerator = {
+            heading: row => row.heading
+        };
+
+        this.rowFlairCallback = (row: any): GridRowFlairEntry => {
+            const flair = {icon: null, title: null};
+            if (this.browse.markedForMerge[row.authority.id()]) {
+                flair.icon = 'merge_type';
+                flair.title = this.rowSelected.text;
+            }
+            return flair;
+        };
+    }
+
+
+    markForMerge(rows: any[]) {
+        rows.forEach(row =>
+            this.browse.markedForMerge[row.authority.id()] = row);
+    }
+
+    unMarkForMerge(rows: any[]) {
+        rows.forEach(row =>
+            delete this.browse.markedForMerge[row.authority.id()]);
+    }
+
+    clearMergeSelection() {
+        this.browse.markedForMerge = {};
+    }
+
+    search(offset?: number, isNew?: boolean) {
+        if (offset) {
+            this.browse.searchOffset += offset;
+        } else if (isNew) {
+            this.browse.searchOffset = 0;
+        }
+        this.grid.reload();
+    }
+
+    openMergeDialog() {
+        const rows = Object.values(this.browse.markedForMerge);
+        if (rows.length > 0) {
+            this.mergeDialog.authData = rows;
+            this.mergeDialog.open({size: 'lg'}).subscribe(success => {
+                if (success) {
+                    this.clearMergeSelection();
+                    this.search();
+                }
+            });
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/authority/browse.service.ts b/Open-ILS/src/eg2/src/app/staff/cat/authority/browse.service.ts
new file mode 100644 (file)
index 0000000..83701c1
--- /dev/null
@@ -0,0 +1,81 @@
+import {Injectable} from '@angular/core';
+import {Observable, empty} from 'rxjs';
+import {map, switchMap} from 'rxjs/operators';
+import {IdlObject} from '@eg/core/idl.service';
+import {Pager} from '@eg/share/util/pager';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+/* Browse APIS and state maintenance */
+
+@Injectable()
+export class BrowseService {
+
+    // Grid paging is disabled in this UI to support browsing in
+    // both directions.  Define our own paging trackers.
+    pageSize = 15;
+    searchOffset = 0;
+
+    searchTerm: string;
+    authorityAxis: string;
+    authorityAxes: ComboboxEntry[];
+    markedForMerge: {[id: number]: boolean} = {};
+
+    constructor(
+        private net: NetService,
+        private org: OrgService,
+        private pcrud: PcrudService
+    ) {}
+
+    fetchAxes(): Promise<any> {
+        if (this.authorityAxes) {
+            return Promise.resolve(this.authorityAxes);
+        }
+
+        this.pcrud.retrieveAll('aba', {}, {atomic: true})
+        .pipe(map(axes => {
+                this.authorityAxes = axes
+                    .map(axis => ({id: axis.code(), label: axis.name()}))
+                    .sort((a1, a2) => a1.label < a2.label ? -1 : 1);
+        })).toPromise();
+
+    }
+
+    loadAuthorities(): Observable<any> {
+
+        if (!this.searchTerm || !this.authorityAxis) {
+            return empty();
+        }
+
+        return this.net.request(
+            'open-ils.supercat',
+            'open-ils.supercat.authority.browse.by_axis',
+            this.authorityAxis, this.searchTerm,
+            this.pageSize, this.searchOffset
+
+        ).pipe(switchMap(authIds => {
+
+            return this.net.request(
+                'open-ils.search',
+                'open-ils.search.authority.main_entry', authIds
+            );
+
+        })).pipe(map(authMeta => {
+
+            const oOrg = this.org.get(authMeta.authority.owner());
+
+            return {
+                authority: authMeta.authority,
+                link_count: authMeta.linked_bibs.length,
+                heading: authMeta.heading,
+                thesaurus: authMeta.thesaurus,
+                thesaurus_code: authMeta.thesaurus_code,
+                owner: oOrg ? oOrg.shortname() : ''
+            };
+        }));
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/authority/manage.component.html b/Open-ILS/src/eg2/src/app/staff/cat/authority/manage.component.html
new file mode 100644 (file)
index 0000000..37bb7ec
--- /dev/null
@@ -0,0 +1,35 @@
+<eg-staff-banner bannerText="#{{authId}} {{authMeta ? authMeta.heading : ''}}" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="row mb-2">
+  <div class="col-lg-3">
+    <a routerLink="/staff/cat/authority/browse">
+      <button class="btn btn-outline-dark">
+        <span class="material-icons material-mat-icon-shrunk-in-button">arrow_back</span>
+        <span class="pl-1" i18n>Return to Browse</span>
+      </button>
+    </a>
+  </div>
+</div>
+
+<ngb-tabset #authTabs [activeId]="authTab" 
+  (tabChange)="beforeTabChange($event)">
+  <ngb-tab title="Linked Bibs" i18n-title id="bibs">
+    <ng-template ngbTabContent>
+      <div class="mt-3" *ngIf="authMeta">
+        <eg-bib-list #bibList [bibIds]="authMeta.linked_bibs"
+          gridPersistKey="cat.authority.manage.bibs"></eg-bib-list>
+      </div>
+    </ng-template>
+  </ngb-tab>
+  <ngb-tab title="Edit" i18n-title id="edit">
+    <ng-template ngbTabContent>
+      <div class="mt-3">
+        <eg-marc-editor #marcEditor recordType="authority" [recordId]="authId">
+        </eg-marc-editor>
+      </div>
+    </ng-template>
+  </ngb-tab>
+</ngb-tabset>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/authority/manage.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/authority/manage.component.ts
new file mode 100644 (file)
index 0000000..1234e76
--- /dev/null
@@ -0,0 +1,67 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Observable, empty} from 'rxjs';
+import {map, switchMap} from 'rxjs/operators';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {IdlObject} from '@eg/core/idl.service';
+import {Pager} from '@eg/share/util/pager';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+/* Find, merge, and edit authority records */
+
+@Component({
+  templateUrl: 'manage.component.html'
+})
+export class ManageAuthorityComponent implements OnInit {
+
+    authId: number;
+    authTab = 'bibs';
+    authMeta: any;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private net: NetService,
+        private org: OrgService,
+        private pcrud: PcrudService
+    ) {
+    }
+
+    ngOnInit() {
+        this.route.paramMap.subscribe((params: ParamMap) => {
+            this.authTab = params.get('tab') || 'bibs';
+            const id = +params.get('id');
+
+            if (id !== this.authId) {
+                this.authId = id;
+
+                this.net.request(
+                    'open-ils.search',
+                    'open-ils.search.authority.main_entry', this.authId
+                ).subscribe(meta => this.authMeta = meta);
+            }
+        });
+    }
+
+    // Changing a tab in the UI means changing the route.
+    // Changing the route ultimately results in changing the tab.
+    beforeTabChange(evt: NgbTabChangeEvent) {
+
+        // prevent tab changing until after route navigation
+        evt.preventDefault();
+
+        this.authTab = evt.nextId;
+        this.routeToTab();
+    }
+
+    routeToTab() {
+        const url =
+            `/staff/cat/authority/manage/${this.authId}/${this.authTab}`;
+        this.router.navigate([url]);
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/authority/merge-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/cat/authority/merge-dialog.component.html
new file mode 100644 (file)
index 0000000..cccf2ee
--- /dev/null
@@ -0,0 +1,42 @@
+
+<eg-string #successMsg
+    text="Successfully Merged Authority Records" i18n-text></eg-string>
+<eg-string #errorMsg 
+    text="Failed To Merge Authority Records" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title">
+      <span i18n>Merge Authority Records</span>
+    </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 d-flex justify-content-center">
+      <h5>Merge {{authData.length}} Records?</h5>
+    </div>
+    <div class="row">
+      <div class="col-lg-2" i18n>Lead Record</div>
+    </div>
+    <div class="row" *ngFor="let data of authData">
+      <div class="col-lg-2">
+        <input type="radio" name="leadRecord" 
+          [value]="data.authority.id()" [(ngModel)]="leadRecord"/>
+      </div>
+      <div class="col-lg-1">#{{data.authority.id()}}</div>
+      <div class="col-lg-6">{{data.heading}}</div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <ng-container *ngIf="!chargeResponse">
+      <button type="button" class="btn btn-warning" 
+        (click)="close()" i18n>Cancel</button>
+      <button type="button" class="btn btn-success" 
+        (click)="merge()" i18n>Merge Records</button>
+    </ng-container>
+  </div>
+</ng-template>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/authority/merge-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/authority/merge-dialog.component.ts
new file mode 100644 (file)
index 0000000..526121e
--- /dev/null
@@ -0,0 +1,74 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+
+/**
+ * Dialog for merging authority records.
+ */
+
+@Component({
+  selector: 'eg-authority-merge-dialog',
+  templateUrl: 'merge-dialog.component.html'
+})
+
+export class AuthorityMergeDialogComponent
+    extends DialogComponent implements OnInit {
+
+    // Rows passed from the authority browse grid.
+    @Input() authData: any[] = [];
+
+    leadRecord: number;
+
+    @ViewChild('successMsg', {static: true})
+        private successMsg: StringComponent;
+
+    @ViewChild('errorMsg', {static: true})
+        private errorMsg: StringComponent;
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private toast: ToastService,
+        private net: NetService,
+        private evt: EventService,
+        private auth: AuthService) {
+        super(modal); // required for subclassing
+    }
+
+    ngOnInit() {
+        this.onOpen$.subscribe(_ => {
+            if (this.authData.length > 0) {
+                this.leadRecord = this.authData[0].authority.id();
+            }
+        });
+    }
+
+    merge() {
+
+        const list = this.authData
+            .map(data => data.authority.id())
+            .filter(id => id !== this.leadRecord);
+
+        this.net.request('open-ils.cat',
+            'open-ils.cat.authority.records.merge',
+            this.auth.token(), this.leadRecord, list)
+        .subscribe(resp => {
+            const evt = this.evt.parse(resp);
+
+            if (evt) {
+                this.errorMsg.current().then(str => this.toast.warning(str));
+                this.close(false);
+            } else {
+                this.successMsg.current().then(str => this.toast.success(str));
+                this.close(true);
+            }
+        });
+    }
+}
+
+
+
index cd6b3a1..b3fcf36 100644 (file)
@@ -1,6 +1,8 @@
 import {NgModule} from '@angular/core';
 import {RouterModule, Routes} from '@angular/router';
 import {AuthorityMarcEditComponent} from './marc-edit.component';
+import {BrowseAuthorityComponent} from './browse.component';
+import {ManageAuthorityComponent} from './manage.component';
 
 const routes: Routes = [{
     path: 'edit',
@@ -8,6 +10,15 @@ const routes: Routes = [{
   }, {
     path: 'edit/:id',
     component: AuthorityMarcEditComponent
+  }, {
+    path: 'browse',
+    component: BrowseAuthorityComponent
+  }, {
+    path: 'manage/:id/:tab',
+    component: ManageAuthorityComponent
+  }, {
+    path: 'manage/:id/:tab',
+    component: ManageAuthorityComponent
 }];
 
 @NgModule({
index ad4a3d8..a36ce00 100644 (file)
             <span i18n>Link Checker</span>
           </a>
           <div class="dropdown-divider"></div>
-          <a href="/eg/staff/cat/catalog/manageAuthorities" class="dropdown-item">
+          <a routerLink="/staff/cat/authority/browse" class="dropdown-item">
             <span class="material-icons" aria-hidden="true">lock</span>
             <span i18n>Manage Authorities</span>
           </a>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/bib-list/bib-list.component.html b/Open-ILS/src/eg2/src/app/staff/share/bib-list/bib-list.component.html
new file mode 100644 (file)
index 0000000..6c0ffca
--- /dev/null
@@ -0,0 +1,31 @@
+<ng-template #titleTemplate let-row="row">
+  <a routerLink="/staff/catalog/record/{{row.id()}}"
+    i18n-title title="View Record {{row.id()}}">
+    {{row.title()}}
+  </a>
+</ng-template>
+
+<eg-grid #grid [dataSource]="dataSource" idlClass="rmsr" [sortable]="true"
+  [cellTextGenerator]="cellTextGenerator" [persistKey]="gridPersistKey"
+  [showDeclaredFieldsOnly]="true">
+
+  <eg-grid-column name="id" label="ID" i18n-label flex="1"></eg-grid-column>
+
+  <eg-grid-column name="title" [cellTemplate]="titleTemplate"
+    label="Title" i18n-label flex="3"></eg-grid-column>
+
+  <eg-grid-column name="author" label="Author" i18n-label></eg-grid-column>
+
+  <eg-grid-column name="creator" label="Creator" i18n-label [sortable]="false"
+    path="biblio_record.creator.usrname" flex="1"></eg-grid-column>
+
+  <eg-grid-column name="create_date" label="Create Date" i18n-label
+    [sortable]="false" path="biblio_record.create_date" flex="1"></eg-grid-column>
+
+  <eg-grid-column name="editor" label="Editor" i18n-label [sortable]="false"
+    path="biblio_record.editor.usrname" flex="1"></eg-grid-column>
+
+  <eg-grid-column name="edit_date" label="Edit Date" i18n-label
+    [sortable]="false" path="biblio_record.edit_date" flex="1"></eg-grid-column>
+
+</eg-grid>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/bib-list/bib-list.component.ts b/Open-ILS/src/eg2/src/app/staff/share/bib-list/bib-list.component.ts
new file mode 100644 (file)
index 0000000..19faff3
--- /dev/null
@@ -0,0 +1,78 @@
+import {Component, Input, OnInit, ViewChild} from '@angular/core';
+import {Observable, empty} from 'rxjs';
+import {map, switchMap} from 'rxjs/operators';
+import {IdlObject} from '@eg/core/idl.service';
+import {Pager} from '@eg/share/util/pager';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridContext, GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+
+/* Grid of bib records and associated actions. */
+
+@Component({
+  templateUrl: 'bib-list.component.html',
+  selector: 'eg-bib-list'
+})
+export class BibListComponent implements OnInit {
+
+    // Display bibs linked to this authority record.
+    @Input() bibIds: number[];
+    @Input() gridPersistKey: string;
+
+    dataSource: GridDataSource;
+    cellTextGenerator: GridCellTextGenerator;
+
+    @ViewChild('grid', {static: false}) grid: GridComponent;
+
+    constructor(
+        private net: NetService,
+        private org: OrgService,
+        private pcrud: PcrudService
+    ) {
+    }
+
+    ngOnInit() {
+        this.dataSource = new GridDataSource();
+
+        this.dataSource.getRows = (pager: Pager, sort: any): Observable<any> => {
+
+            if (this.bibIds) {
+                return this.loadIds(pager, sort);
+            }
+
+            return empty();
+        };
+
+        this.cellTextGenerator = {
+            title: row => row.title
+        };
+    }
+
+    loadIds(pager: Pager, sort: any): Observable<any> {
+        if (this.bibIds.length === 0) {
+            return empty();
+        }
+
+        const orderBy: any = {rmsr: 'title'};
+        if (sort.length) {
+            orderBy.rmsr = sort[0].name + ' ' + sort[0].dir;
+        }
+
+        return this.pcrud.search('rmsr', {id: this.bibIds}, {
+            order_by: orderBy,
+            limit: pager.limit,
+            offset: pager.offset,
+            flesh: 2,
+            flesh_fields: {
+                rmsr: ['biblio_record'],
+                bre: ['creator', 'editor']
+            }
+        });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/bib-list/bib-list.module.ts b/Open-ILS/src/eg2/src/app/staff/share/bib-list/bib-list.module.ts
new file mode 100644 (file)
index 0000000..3767156
--- /dev/null
@@ -0,0 +1,20 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {BibListComponent} from './bib-list.component';
+
+@NgModule({
+    declarations: [
+      BibListComponent
+    ],
+    imports: [
+        StaffCommonModule
+    ],
+    exports: [
+      BibListComponent
+    ],
+    providers: [
+    ]
+})
+
+export class BibListModule {}
+
index 2e90ae2..57ec61d 100644 (file)
@@ -4,6 +4,9 @@ use strict; use warnings;
 
 use OpenILS::Utils::Fieldmapper;
 use OpenILS::Application::AppUtils;
+use MARC::Record;
+use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
 use XML::LibXML;
 use XML::LibXSLT;
 use OpenILS::Utils::CStoreEditor q/:funcs/;
@@ -338,6 +341,92 @@ sub crossref_authority_batch2 {
     return $response;
 }
 
+__PACKAGE__->register_method(
+    method        => "authority_main_entry",
+    api_name      => "open-ils.search.authority.main_entry",
+    stream => 1,
+    signature     => {
+        desc => q/
+            Returns the main entry details for one or more authority 
+            records plus a few other details.
+        /,
+        params => [
+            {desc => 'Authority IDs', type => 'number or array'}
+        ],
+        return => {
+            desc => q/
+                Stream of authority metadata objects.
+                {   authority: are_object,
+                    heading: heading_text,
+                    thesaurus: short_code,
+                    thesaurus_code: code,
+                    control_set: control_set_object,
+                    linked_bibs: [id1, id2, ...]
+                }
+            /,
+            type => 'object'
+        }
+    }
+);
+
+sub authority_main_entry {
+    my ($self, $client, $auth_ids) = @_;
+
+    $auth_ids = [$auth_ids] unless ref $auth_ids;
+
+    my $e = new_editor();
+
+    for my $auth_id (@$auth_ids) {
+
+        my $rec = $e->retrieve_authority_record_entry([
+            $auth_id, {
+                flesh => 1,
+                flesh_fields => {are => [qw/control_set bib_links creator/]}
+            }
+        ]) or return $e->event;
+
+        my $response = {
+            authority => $rec,
+            control_set => $rec->control_set,
+            linked_bibs => [ map {$_->bib} @{$rec->bib_links} ]
+        };
+
+        # Extract the heading and thesaurus.
+        # In theory this data has already been extracted in the DB, but
+        # using authority.simple_heading results in data that varies
+        # quite a bit from the previous authority manage interface.  I
+        # took the MARC parsing approach because it matches the logic
+        # (and results) of the previous UI.
+
+        my $marc = MARC::Record->new_from_xml($rec->marc);
+        my $heading_field = $marc->field('1..');
+        $response->{heading} = $heading_field->as_string if $heading_field;
+
+        my $field_008 = $marc->field('008');
+        if ($field_008) {
+
+            # Extract the 1-char thesaurus code from the 008.
+            my $thes = substr($field_008->data, 11, 1);
+
+            if (defined $thes) {
+                $response->{thesaurus} = $thes;
+
+                if ($thes ne 'z') { # 'z' ('Other') maps to many entries
+                    my $thesaurus = $e->search_authority_thesaurus(
+                        {short_code => $thes})->[0];
+
+                    $response->{thesaurus_code} = $thesaurus->code if $thesaurus;
+                }
+            }
+        }
+
+        $rec->clear_marc;
+        $client->respond($response);
+    }
+
+    return undef;
+}
+
 
 
 1;
index fe6de39..1856498 100644 (file)
@@ -20520,6 +20520,18 @@ VALUES (
         'Default org unit for patron search',
         'cwst', 'label'
     )
+), (
+    'eg.grid.cat.authority.browse', 'gui', 'object',
+    oils_i18n_gettext(
+    'eg.grid.cat.authority.browse',
+    'Grid Config: eg.grid.cat.authority.browse',
+    'cwst', 'label')
+), (
+    'eg.grid.cat.authority.manage.bibs', 'gui', 'object',
+    oils_i18n_gettext(
+    'eg.grid.cat.authority.manage.bibs',
+    'Grid Config: eg.grid.cat.authority.manage.bibs',
+    'cwst', 'label')
 );
 
 INSERT INTO config.workstation_setting_type
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.manage-authority-grids.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.manage-authority-grids.sql
new file mode 100644 (file)
index 0000000..4236704
--- /dev/null
@@ -0,0 +1,20 @@
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('TODO', :eg_version);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.grid.cat.authority.browse', 'gui', 'object',
+    oils_i18n_gettext(
+    'eg.grid.cat.authority.browse',
+    'Grid Config: eg.grid.cat.authority.browse',
+    'cwst', 'label')
+), (
+    'eg.grid.cat.authority.manage.bibs', 'gui', 'object',
+    oils_i18n_gettext(
+    'eg.grid.cat.authority.manage.bibs',
+    'Grid Config: eg.grid.cat.authority.manage.bibs',
+    'cwst', 'label')
+);
+
+COMMIT;
index 6ccd5b4..48b808f 100644 (file)
           </li>
           <li class="divider"></li>
           <li>
-            <a href="./cat/catalog/manageAuthorities" target="_self">
+            <a href="/eg2/staff/cat/authority/browse">
               <span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
               [% l('Manage Authorities') %]
             </a>
diff --git a/docs/RELEASE_NOTES_NEXT/Cataloging/manage-auths-ang.adoc b/docs/RELEASE_NOTES_NEXT/Cataloging/manage-auths-ang.adoc
new file mode 100644 (file)
index 0000000..a044ea5
--- /dev/null
@@ -0,0 +1,7 @@
+Manage Authorities Angular Port
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The Cataloging => 'Manage Authorities' interface has been ported to Angular.
+
+New functionality includes displaying additional authority data, like create
+and edit dates, etc.  It's also possible to view the list of linked bib
+records.