<field reporter:label="Last Editing User" name="editor" reporter:datatype="link"/>
<field reporter:label="Fine Level" name="fine_level" reporter:datatype="int"/>
<field reporter:label="Is Holdable" name="holdable" reporter:datatype="bool" />
- <field reporter:label="Copy ID" name="id" reporter:datatype="id"/>
+ <field reporter:label="Copy ID" name="id" reporter:selector="barcode" reporter:datatype="id"/>
<field reporter:label="Loan Duration" name="loan_duration" reporter:datatype="int"/>
<field reporter:label="Shelving Location" name="location" reporter:datatype="link"/>
<field reporter:label="OPAC Visible" name="opac_visible" reporter:datatype="bool" />
export class GridToolbarActionComponent implements OnInit {
+ toolbarAction: GridToolbarAction;
+
// Note most input fields should match class fields for GridColumn
@Input() label: string;
// Register to click events
@Output() onClick: EventEmitter<any []>;
+ // When present, actions will be grouped by the provided label.
+ @Input() group: string;
+
// DEPRECATED: Pass a reference to a function that is called on click.
@Input() action: (rows: any[]) => any;
- // When present, actions will be grouped by the provided label.
- @Input() group: string;
+ @Input() set disabled(d: boolean) {
+ this.toolbarAction.disabled = d;
+ }
+ get disabled(): boolean {
+ return this.toolbarAction.disabled;
+ }
// Optional: add a function that returns true or false.
// If true, this action will be disabled; if false
// get a reference to our container grid.
constructor(@Host() private grid: GridComponent) {
this.onClick = new EventEmitter<any []>();
+ this.toolbarAction = new GridToolbarAction();
}
ngOnInit() {
return;
}
- const action = new GridToolbarAction();
- action.label = this.label;
- action.action = this.action;
- action.onClick = this.onClick;
- action.group = this.group;
- action.disableOnRows = this.disableOnRows;
- this.grid.context.toolbarActions.push(action);
+ if (this.action) {
+ console.debug('toolbar [action] is deprecated. use (onClick) instead.')
+ }
+
+ this.toolbarAction.label = this.label;
+ this.toolbarAction.onClick = this.onClick;
+ this.toolbarAction.group = this.group;
+ this.toolbarAction.action = this.action;
+ this.toolbarAction.disabled = this.disabled;
+ this.toolbarAction.disableOnRows = this.disableOnRows;
+ this.grid.context.toolbarActions.push(this.toolbarAction);
}
}
onClick: EventEmitter<any []>;
action: (rows: any[]) => any; // DEPRECATED
group: string;
+ disabled: boolean;
isGroup: boolean; // used for group placeholder entries
disableOnRows: (rows: any[]) => boolean;
}
import {BrowseComponent} from './browse.component';
import {BrowseResultsComponent} from './browse/results.component';
import {HoldingsMaintenanceComponent} from './record/holdings.component';
+import {ConjoinedComponent} from './record/conjoined.component';
@NgModule({
declarations: [
PartMergeDialogComponent,
BrowseComponent,
BrowseResultsComponent,
+ ConjoinedComponent,
HoldingsMaintenanceComponent
],
imports: [
--- /dev/null
+
+
+<eg-conjoined-items-dialog #conjoinedDialog
+ [peerRecord]="recordId" [modifyAll]="true">
+</eg-conjoined-items-dialog>
+
+<eg-confirm-dialog #confirmUnlink
+ i18n-dialogTitle dialogTitle="Confirm Unlink"
+ i18n-dialogBody dialogBody="Unlink {{idsToUnlink.length}} Items?">
+</eg-confirm-dialog>
+
+<div class="mt-3">
+
+ <eg-grid #conjoinedGrid idlClass="bpbcm" [dataSource]="gridDataSource"
+ [sortable]="true" persistKey="catalog.record.conjoined" class="mt-3">
+
+ <eg-grid-toolbar-button [disabled]="!hasPerm" i18n-label
+ label="Change Type" (onClick)="openConjoinedDialog()">
+ </eg-grid-toolbar-button>
+
+ <eg-grid-toolbar-action
+ [disabled]="!hasPerm"
+ label="Unlink" i18n-label (onClick)="unlink($event)">
+ </eg-grid-toolbar-action>
+
+ </eg-grid>
+</div>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {Pager} from '@eg/share/util/pager';
+import {OrgService} from '@eg/core/org.service';
+import {PermService} from '@eg/core/perm.service';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {ConjoinedItemsDialogComponent
+ } from '@eg/staff/share/holdings/conjoined-items-dialog.component';
+
+/** Conjoined items per record grid */
+
+@Component({
+ selector: 'eg-catalog-record-conjoined',
+ templateUrl: 'conjoined.component.html'
+})
+export class ConjoinedComponent implements OnInit {
+
+ @Input() recordId: number;
+
+ hasPerm: boolean;
+ gridDataSource: GridDataSource;
+ idsToUnlink: number[];
+
+ @ViewChild('conjoinedGrid') private grid: GridComponent;
+
+ @ViewChild('conjoinedDialog')
+ private conjoinedDialog: ConjoinedItemsDialogComponent;
+
+ @ViewChild('confirmUnlink')
+ private confirmUnlink: ConfirmDialogComponent;
+
+ constructor(
+ private idl: IdlService,
+ private org: OrgService,
+ private pcrud: PcrudService,
+ private perm: PermService
+ ) {
+ this.gridDataSource = new GridDataSource();
+ this.idsToUnlink = [];
+ }
+
+ ngOnInit() {
+ // Load edit perms
+ this.perm.hasWorkPermHere(['UPDATE_COPY'])
+ .then(perms => this.hasPerm = perms.UPDATE_COPY);
+
+ this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+ const orderBy: any = {};
+
+ if (sort.length) { // Sort provided by grid.
+ orderBy.bmp = sort[0].name + ' ' + sort[0].dir;
+ } else {
+ orderBy.bmp = 'id';
+ }
+
+ const searchOps = {
+ offset: pager.offset,
+ limit: pager.limit,
+ order_by: orderBy
+ };
+
+ return this.pcrud.search('bpbcm',
+ {peer_record: this.recordId}, searchOps, {fleshSelectors: true});
+ };
+ }
+
+ async unlink(rows: any) {
+
+ this.idsToUnlink = rows.map(r => r.target_copy().id());
+ if (this.idsToUnlink.length === 0) { return; }
+
+ try { // rejects on dismiss, which results in an Error
+ await this.confirmUnlink.open({size: 'sm'});
+ } catch (dismissed) {return;}
+
+ const maps = [];
+ this.pcrud.search('bpbcm',
+ {target_copy: this.idsToUnlink, peer_record: this.recordId})
+ .subscribe(
+ map => maps.push(map),
+ err => {},
+ () => {
+ this.pcrud.remove(maps).subscribe(
+ ok => console.debug('deleted map ', ok),
+ err => console.error(err),
+ () => {
+ this.idsToUnlink = [];
+ this.grid.reload();
+ }
+ )
+ }
+ );
+ }
+
+ openConjoinedDialog() {
+ this.conjoinedDialog.open({size: 'sm'}).then(
+ modified => {
+ if (modified) {
+ this.grid.reload();
+ }
+ },
+ notOk => {}
+ );
+ }
+}
+
</ngb-tab>
<ngb-tab title="Conjoined Items" i18n-title id="conjoined">
<ng-template ngbTabContent>
- <div class="alert alert-info mt-3" i18n>
- Conjoined Items not yet implemented. See the
- <a target="_blank"
- href="/eg/staff/cat/catalog/record/{{recordId}}/conjoined">
- AngularJS Conjoined Items Tab.
- </a>
- </div>
+ <eg-catalog-record-conjoined [recordId]="recordId">
+ </eg-catalog-record-conjoined>
</ng-template>
</ngb-tab>
</ngb-tabset>
<eg-string #successMsg
- text="Successfully Attached Conjoined Item(s)" i18n-text></eg-string>
+ text="Successfully Attached/Modified Conjoined Item(s)" i18n-text></eg-string>
<eg-string #errorMsg
- text="Failed To Attach Conjoined Item(s)" i18n-text></eg-string>
+ text="Failed To Attach/Modify Conjoined Item(s)" i18n-text></eg-string>
<ng-template #dialogContent>
<div class="modal-header bg-info">
<h4 class="modal-title">
- <span i18n>Attach {{copyIds.length}} Conjoined Item(s)</span>
+ <span i18n>Attach/Modify {{copyIds.length}} Conjoined Item(s)</span>
</h4>
<button type="button" class="close"
i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
(click)="dismiss('canceled')" i18n>Cancel</button>
<button type="button" class="btn btn-success"
(click)="linkCopies()" [disabled]="!peerType" i18n>
- Attach
+ Attach/Modify
</button>
</ng-container>
</div>
import {Component, OnInit, OnDestroy, Input, ViewChild, Renderer2} from '@angular/core';
import {Subscription} from 'rxjs';
-import {IdlService} from '@eg/core/idl.service';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
import {PcrudService} from '@eg/core/pcrud.service';
import {ToastService} from '@eg/share/toast/toast.service';
import {StoreService} from '@eg/core/store.service';
extends DialogComponent implements OnInit, OnDestroy {
@Input() copyIds: number[];
- ids: number[]; // copy of list so we can pop()
+
+ // If true, ignore the provided copyIds array and fetch all of
+ // the linked copies to work on.
+ @Input() modifyAll: boolean;
+
+ // If peerRecord is not set, the localStorage value will be used.
+ @Input() peerRecord: number;
peerType: number;
numSucceeded: number;
numFailed: number;
peerTypes: ComboboxEntry[];
- peerRecord: number;
existingMaps: any;
-
onOpenSub: Subscription;
- @ViewChild('successMsg')
- private successMsg: StringComponent;
-
- @ViewChild('errorMsg')
- private errorMsg: StringComponent;
+ @ViewChild('successMsg') private successMsg: StringComponent;
+ @ViewChild('errorMsg') private errorMsg: StringComponent;
constructor(
private modal: NgbModal, // required for passing to parent
private localStore: StoreService) {
super(modal); // required for subclassing
this.peerTypes = [];
+ this.copyIds = [];
}
ngOnInit() {
this.onOpenSub = this.onOpen$.subscribe(() => {
- this.ids = [].concat(this.copyIds);
+ if (this.modifyAll) {
+ // This will be set once the list of copies to
+ // modify has been fetched.
+ this.copyIds = [];
+ }
this.numSucceeded = 0;
this.numFailed = 0;
- this.peerRecord =
- this.localStore.getLocalItem('eg.cat.marked_conjoined_record');
if (!this.peerRecord) {
- this.close(false);
+ this.peerRecord =
+ this.localStore.getLocalItem('eg.cat.marked_conjoined_record');
+
+ if (!this.peerRecord) {
+ this.close(false);
+ }
}
if (this.peerTypes.length === 0) {
this.getPeerTypes();
}
- this.fetchExisting();
+ this.fetchExistingMaps();
});
}
this.onOpenSub.unsubscribe();
}
- fetchExisting() {
+ fetchExistingMaps() {
this.existingMaps = {};
- this.pcrud.search('bpbcm',
- {target_copy: this.copyIds, peer_record: this.peerRecord})
- .subscribe(map => this.existingMaps[map.target_copy()] = map);
+ const search: any = {
+ peer_record: this.peerRecord
+ };
+
+ if (!this.modifyAll) {
+ search.target_copy = this.copyIds;
+ }
+
+ this.pcrud.search('bpbcm', search)
+ .subscribe(map => {
+ this.existingMaps[map.target_copy()] = map;
+ if (this.modifyAll) {
+ this.copyIds.push(map.target_copy());
+ }
+ });
}
+ // Fetch and map peer types to combobox entries
getPeerTypes(): Promise<any> {
return this.pcrud.retrieveAll('bpt', {}, {atomic: true}).toPromise()
.then(types =>
- // Map types to ComboboxEntry's
this.peerTypes = types.map(t => ({id: t.id(), label: t.name()}))
);
}
}
}
- linkCopies(): Promise<any> {
-
- if (this.ids.length === 0) {
- this.close(this.numSucceeded > 0);
- return Promise.resolve();
- }
-
- const id = this.ids.pop();
- const map = this.existingMaps[id] || this.idl.create('bpbcm');
- map.peer_record(this.peerRecord);
- map.target_copy(id);
- map.peer_type(this.peerType);
+ // Create or update peer copy links.
+ linkCopies() {
+
+ const maps: IdlObject[] = [];
+ this.copyIds.forEach(id => {
+ let map: IdlObject;
+ if (this.existingMaps[id]) {
+ map = this.existingMaps[id];
+ map.ischanged(true);
+ } else {
+ map = this.idl.create('bpbcm');
+ map.isnew(true);
+ }
- let promise: Promise<any>;
- if (this.existingMaps[id]) {
- promise = this.pcrud.update(map).toPromise();
- } else {
- promise = this.pcrud.create(map).toPromise();
- }
+ map.peer_record(this.peerRecord);
+ map.target_copy(id);
+ map.peer_type(this.peerType);
+ maps.push(map);
+ });
- return promise.then(
- ok => {
- this.successMsg.current().then(msg => this.toast.success(msg));
- this.numSucceeded++;
- return this.linkCopies();
- },
+ return this.pcrud.autoApply(maps).subscribe(
+ ok => this.numSucceeded++,
err => {
this.numFailed++;
console.error(err);
this.errorMsg.current().then(msg => this.toast.warning(msg));
- return this.linkCopies();
+ },
+ () => {
+ this.successMsg.current().then(msg => this.toast.success(msg));
+ this.close(this.numSucceeded > 0);
}
);
}