'indexing': 'http://open-ils.org/spec/indexing/v1'
};
-export const HOLDINGS_XPATH =
+export const HOLDINGS_XPATH =
'/holdings:holdings/holdings:counts/holdings:count';
+
+export class BibRecordSummary {
+ id: number; // == record.id() for convenience
+ orgId: number;
+ orgDepth: number;
+ record: IdlObject;
+ display: any;
+ attributes: any;
+ holdingsSummary: any;
+ holdCount: number;
+ bibCallNumber: string;
+ net: NetService;
+
+ constructor(record: IdlObject, orgId: number, orgDepth: number) {
+ this.id = record.id();
+ this.record = record;
+ this.orgId = orgId;
+ this.orgDepth = orgDepth;
+ this.display = {};
+ this.attributes = {};
+ this.bibCallNumber = null;
+ }
+
+ ingest() {
+ this.compileDisplayFields();
+ this.compileRecordAttrs();
+
+ // Normalize some data for JS consistency
+ this.record.creator(Number(this.record.creator()));
+ this.record.editor(Number(this.record.editor()));
+ }
+
+ compileDisplayFields() {
+ this.record.flat_display_entries().forEach(entry => {
+ if (entry.multi() === 't') {
+ if (this.display[entry.name()]) {
+ this.display[entry.name()].push(entry.value());
+ } else {
+ this.display[entry.name()] = [entry.value()];
+ }
+ } else {
+ this.display[entry.name()] = entry.value();
+ }
+ });
+ }
+
+ compileRecordAttrs() {
+ // Any attr can be multi-valued.
+ this.record.mattrs().forEach(attr => {
+ if (this.attributes[attr.attr()]) {
+ this.attributes[attr.attr()].push(attr.value());
+ } else {
+ this.attributes[attr.attr()] = [attr.value()];
+ }
+ });
+ }
+
+ // Get -> Set -> Return bib hold count
+ getHoldCount(): Promise<number> {
+
+ if (Number.isInteger(this.holdCount)) {
+ return Promise.resolve(this.holdCount);
+ }
+
+ return this.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.bre.holds.count', this.id
+ ).toPromise().then(count => this.holdCount = count);
+ }
+
+ // Get -> Set -> Return bib-level call number
+ getBibCallNumber(): Promise<string> {
+
+ if (this.bibCallNumber !== null) {
+ return Promise.resolve(this.bibCallNumber);
+ }
+
+ // TODO labelClass = cat.default_classification_scheme YAOUS
+ const labelClass = 1;
+
+ return this.net.request(
+ 'open-ils.cat',
+ 'open-ils.cat.biblio.record.marc_cn.retrieve',
+ this.id, labelClass
+ ).toPromise().then(cnArray => {
+ if (cnArray && cnArray.length > 0) {
+ const key1 = Object.keys(cnArray[0])[0];
+ this.bibCallNumber = cnArray[0][key1];
+ } else {
+ this.bibCallNumber = '';
+ }
+ return this.bibCallNumber;
+ });
+ }
+}
+
@Injectable()
export class BibRecordService {
// Note when multiple IDs are provided, responses are emitted in order
// of receipt, not necessarily in the requested ID order.
- getBibSummary(bibIds: number | number[],
+ getBibSummary(bibIds: number | number[],
orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
const ids = [].concat(bibIds);
return this.pcrud.search('bre', {id: ids},
{ flesh: 1,
- flesh_fields: {bre: ['flat_display_entries', 'mattrs']},
+ flesh_fields: {bre: ['flat_display_entries', 'mattrs']},
select: {bre : this.fetchableBreFields()}
},
{anonymous: true} // skip unneccesary auth
}
// Flesh the creator and editor fields.
- // Handling this separately lets us pull from the cache and
- // avoids the requirement that the main bib query use a staff
+ // Handling this separately lets us pull from the cache and
+ // avoids the requirement that the main bib query use a staff
// (VIEW_USER) auth token.
fleshBibUsers(records: IdlObject[]): Promise<void> {
})).toPromise();
}
- getHoldingsSummary(recordId: number,
+ getHoldingsSummary(recordId: number,
orgId: number, orgDepth: number): Promise<any> {
const holdingsSummary = [];
};
// Extract the holdings data from the unapi xml doc
- const result = xmlDoc.evaluate(HOLDINGS_XPATH,
+ const result = xmlDoc.evaluate(HOLDINGS_XPATH,
xmlDoc, resolver, XPathResult.ANY_TYPE, null);
let node;
}
}
-export class BibRecordSummary {
-
- id: number; // == record.id() for convenience
- orgId: number;
- orgDepth: number;
- record: IdlObject;
- display: any;
- attributes: any;
- holdingsSummary: any;
- holdCount: number;
- bibCallNumber: string;
- net: NetService;
-
- constructor(record: IdlObject, orgId: number, orgDepth: number) {
- this.id = record.id();
- this.record = record;
- this.orgId = orgId;
- this.orgDepth = orgDepth;
- this.display = {};
- this.attributes = {};
- this.bibCallNumber = null;
- }
-
- ingest() {
- this.compileDisplayFields();
- this.compileRecordAttrs();
-
- // Normalize some data for JS consistency
- this.record.creator(Number(this.record.creator()));
- this.record.editor(Number(this.record.editor()));
- }
-
- compileDisplayFields() {
- this.record.flat_display_entries().forEach(entry => {
- if (entry.multi() === 't') {
- if (this.display[entry.name()]) {
- this.display[entry.name()].push(entry.value());
- } else {
- this.display[entry.name()] = [entry.value()];
- }
- } else {
- this.display[entry.name()] = entry.value();
- }
- });
- }
-
- compileRecordAttrs() {
- // Any attr can be multi-valued.
- this.record.mattrs().forEach(attr => {
- if (this.attributes[attr.attr()]) {
- this.attributes[attr.attr()].push(attr.value());
- } else {
- this.attributes[attr.attr()] = [attr.value()];
- }
- });
- }
-
- // Get -> Set -> Return bib hold count
- getHoldCount(): Promise<number> {
-
- if (Number.isInteger(this.holdCount)) {
- return Promise.resolve(this.holdCount);
- }
-
- return this.net.request(
- 'open-ils.circ',
- 'open-ils.circ.bre.holds.count', this.id
- ).toPromise().then(count => this.holdCount = count);
- }
-
- // Get -> Set -> Return bib-level call number
- getBibCallNumber(): Promise<string> {
-
- if (this.bibCallNumber !== null) {
- return Promise.resolve(this.bibCallNumber);
- }
-
- // TODO labelClass = cat.default_classification_scheme YAOUS
- const labelClass = 1;
-
- return this.net.request(
- 'open-ils.cat',
- 'open-ils.cat.biblio.record.marc_cn.retrieve',
- this.id, labelClass
- ).toPromise().then(cnArray => {
- if (cnArray && cnArray.length > 0) {
- const key1 = Object.keys(cnArray[0])[0];
- this.bibCallNumber = cnArray[0][key1];
- } else {
- this.bibCallNumber = '';
- }
- return this.bibCallNumber;
- });
- }
-}
-
-
private org: OrgService,
private unapi: UnapiService,
private pcrud: PcrudService,
- private bibService: BibRecordService
+ private bibService: BibRecordService
) {}
search(ctx: CatalogSearchContext): Promise<void> {
ctx.currentResultIds(), ctx.searchOrg.id(), depth)
.pipe(map(summary => {
// Responses are not necessarily returned in request-ID order.
- const idx = ctx.currentResultIds().indexOf(summary.record.id());
+ const idx = ctx.currentResultIds().indexOf(summary.record.id());
if (ctx.result.records) {
// May be reset when quickly navigating results.
ctx.result.records[idx] = summary;
<span *ngIf="!column.cellTemplate"
ngbTooltip="{{context.getRowColumnValue(row, column)}}"
- class="{{cellClassCallback(row, column)}}"
+ class="{{context.cellClassCallback(row, column)}}"
triggers="mouseenter:mouseleave">
{{context.getRowColumnValue(row, column)}}
</span>
<span *ngIf="column.cellTemplate"
- class="{{cellClassCallback(row, column)}}"
+ class="{{context.cellClassCallback(row, column)}}"
[ngbTooltip]="column.cellTemplate"
#tooltip="ngbTooltip"
(mouseenter)="tooltip.open(column.getCellContext(row))"
@Input() context: GridContext;
@Input() row: any;
@Input() column: GridColumn;
- @Input() cellClassCallback: (row: any, col: GridColumn) => string;
constructor() {}
- ngOnInit() {
- if (!this.cellClassCallback) {
- this.cellClassCallback = (row: any, col: GridColumn) => '';
- }
- }
+ ngOnInit() {}
}
tabindex=1 so the grid body can capture keyboard events.
-->
<div class="eg-grid-body" tabindex="1" (keydown)="onGridKeyDown($event)">
- <div class="eg-grid-row eg-grid-body-row {{rowClassCallback(row)}}"
+ <div class="eg-grid-row eg-grid-body-row {{context.rowClassCallback(row)}}"
[ngClass]="{'selected': context.rowSelector.contains(context.getRowIndex(row))}"
*ngFor="let row of context.dataSource.getPageOfRows(context.pager); let idx = index">
<div class="eg-grid-cell eg-grid-checkbox-cell eg-grid-cell-skinny">
<input type='checkbox' [(ngModel)]="context.rowSelector.indexes[context.getRowIndex(row)]">
</div>
- <div class="eg-grid-cell eg-grid-header-cell eg-grid-number-cell eg-grid-cell-skinny">
+ <div class="eg-grid-cell eg-grid-number-cell eg-grid-cell-skinny">
{{context.pager.rowNumber(idx)}}
</div>
+ <div *ngIf="context.rowFlairIsEnabled" class="eg-grid-cell eg-grid-flair-cell">
+ <span class="material-icons">{{context.rowFlairCallback(row)}}</span>
+ </div>
<div class="eg-grid-cell eg-grid-body-cell" [ngStyle]="{flex:col.flex}"
(dblclick)="onRowDblClick(row)"
(click)="onRowClick($event, row, idx)"
*ngFor="let col of context.columnSet.displayColumns()">
- <eg-grid-body-cell [context]="context" [row]="row" [column]="col"
- [cellClassCallback]="cellClassCallback">
+ <eg-grid-body-cell [context]="context" [row]="row" [column]="col">
</eg-grid-body-cell>
</div>
</div>
export class GridBodyComponent implements OnInit {
@Input() context: GridContext;
- @Input() rowClassCallback: (row: any) => string;
- @Input() cellClassCallback: (row: any, col: GridColumn) => string;
- constructor(@Host() private grid: GridComponent) {
- }
+ constructor(@Host() private grid: GridComponent) {}
- ngOnInit() {
- if (!this.rowClassCallback) {
- this.rowClassCallback = (row: any) => '';
- }
- }
+ ngOnInit() {}
// Not using @HostListener because it only works globally.
onGridKeyDown(evt: KeyboardEvent) {
<div class="eg-grid-cell eg-grid-header-cell eg-grid-number-cell eg-grid-cell-skinny">
<span i18n="number|Row Number Header">#</span>
</div>
- <div *ngFor="let col of gridContext.columnSet.displayColumns()"
+ <div *ngIf="context.rowFlairIsEnabled"
+ class="eg-grid-cell eg-grid-header-cell eg-grid-flair-cell">
+ <span class="material-icons">notifications</span>
+ </div>
+ <div *ngFor="let col of context.columnSet.displayColumns()"
draggable="true"
(dragstart)="dragColumn = col"
(drop)="onColumnDrop(col)"
export class GridHeaderComponent implements OnInit {
- @Input() gridContext: GridContext;
+ @Input() context: GridContext;
+
dragColumn: GridColumn;
constructor() {}
}
onColumnDrop(col: GridColumn) {
- this.gridContext.columnSet.insertBefore(this.dragColumn, col);
- this.gridContext.columnSet.columns.forEach(c => c.isDragTarget = false);
+ this.context.columnSet.insertBefore(this.dragColumn, col);
+ this.context.columnSet.columns.forEach(c => c.isDragTarget = false);
}
sortOneColumn(col: GridColumn) {
let dir = 'ASC';
- const sort = this.gridContext.dataSource.sort;
+ const sort = this.context.dataSource.sort;
if (sort.length && sort[0].name === col.name && sort[0].dir === 'ASC') {
dir = 'DESC';
}
- this.gridContext.dataSource.sort = [{name: col.name, dir: dir}];
- this.gridContext.reload();
+ this.context.dataSource.sort = [{name: col.name, dir: dir}];
+ this.context.reload();
}
// Returns true if the provided column is sorting in the
// specified direction.
isColumnSorting(col: GridColumn, dir: string): boolean {
- const sort = this.gridContext.dataSource.sort.filter(c => c.name === col.name)[0];
+ const sort = this.context.dataSource.sort.filter(c => c.name === col.name)[0];
return sort && sort.dir === dir;
}
handleBatchSelect($event) {
if ($event.target.checked) {
- if (this.gridContext.rowSelector.isEmpty() || !this.allRowsAreSelected()) {
+ if (this.context.rowSelector.isEmpty() || !this.allRowsAreSelected()) {
// clear selections from other pages to avoid confusion.
- this.gridContext.rowSelector.clear();
+ this.context.rowSelector.clear();
this.selectAll();
}
} else {
- this.gridContext.rowSelector.clear();
+ this.context.rowSelector.clear();
}
}
selectAll() {
- const rows = this.gridContext.dataSource.getPageOfRows(this.gridContext.pager);
- const indexes = rows.map(r => this.gridContext.getRowIndex(r));
- this.gridContext.rowSelector.select(indexes);
+ const rows = this.context.dataSource.getPageOfRows(this.context.pager);
+ const indexes = rows.map(r => this.context.getRowIndex(r));
+ this.context.rowSelector.select(indexes);
}
allRowsAreSelected(): boolean {
- const rows = this.gridContext.dataSource.getPageOfRows(this.gridContext.pager);
- const indexes = rows.map(r => this.gridContext.getRowIndex(r));
- return this.gridContext.rowSelector.contains(indexes);
+ const rows = this.context.dataSource.getPageOfRows(this.context.pager);
+ const indexes = rows.map(r => this.context.getRowIndex(r));
+ return this.context.rowSelector.contains(indexes);
}
}
// let the file name describe the grid
this.csvExportFileName = (
- this.gridContext.mainLabel ||
- this.gridContext.persistKey ||
- 'eg_grid_data'
+ this.gridContext.persistKey || 'eg_grid_data'
).replace(/\s+/g, '_') + '.csv';
this.gridContext.gridToCsv().then(csv => {
flex: none;
}
+.eg-grid-flair-cell {
+ /* mat icons currently 22px, unclear why it needs this much space */
+ width: 34px;
+ text-align: center;
+ flex: none;
+}
+
/* depends on width of .eg-grid-cell-skinny */
.eg-grid-column-width-header {
width: 4.4em;
[colWidthConfig]="colWidthConfig">
</eg-grid-toolbar>
- <eg-grid-header [gridContext]="context"></eg-grid-header>
+ <eg-grid-header [context]="context"></eg-grid-header>
<eg-grid-column-width #colWidthConfig [gridContext]="context">
</eg-grid-column-width>
</div>
</div>
- <eg-grid-body [context]="context"
- [rowClassCallback]="rowClassCallback"
- [cellClassCallback]="cellClassCallback">
- </eg-grid-body>
+ <eg-grid-body [context]="context"></eg-grid-body>
</div>
selector: 'eg-grid',
templateUrl: './grid.component.html',
styleUrls: ['grid.component.css'],
- // share grid css globally once imported so all grid component CSS
- // can live in grid.component.css and to avoid multiple copies of
+ // share grid css globally once imported so all grid component CSS
+ // can live in grid.component.css and to avoid multiple copies of
// the CSS when multiple grids are displayed.
encapsulation: ViewEncapsulation.None
})
export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
- @Input() mainLabel: string;
+ // Source of row data.
@Input() dataSource: GridDataSource;
+
+ // IDL class for auto-generation of columns
@Input() idlClass: string;
+
+ // True if any columns are sortable
@Input() sortable: boolean;
+
+ // True if the grid supports sorting of multiple columns at once
@Input() multiSortable: boolean;
+
+ // Storage persist key / per-grid-type unique identifier
+ // The value is prefixed with 'eg.grid.'
@Input() persistKey: string;
+
+ // Prevent selection of multiple rows
@Input() disableMultiSelect: boolean;
+
+ // Show an extra column in the grid where the caller can apply
+ // row-specific flair (material icons).
+ @Input() rowFlairIsEnabled: boolean;
+
+ // Returns a material icon name to display in the flar column
+ // (if enabled) for the given row.
+ @Input() rowFlairCallback: (row: any) => string;
+
+ // Returns a space-separated list of CSS class names to apply to
+ // a given row
@Input() rowClassCallback: (row: any) => string;
+
+ // Returns a space-separated list of CSS class names to apply to
+ // a given cell or all cells in a column.
@Input() cellClassCallback: (row: any, col: GridColumn) => string;
context: GridContext;
// These events are emitted from our grid-body component.
+ // They are defined here for ease of access to the caller.
onRowActivate$: EventEmitter<any>;
onRowClick$: EventEmitter<any>;
}
ngOnInit() {
- this.context.mainLabel = this.mainLabel;
this.context.idlClass = this.idlClass;
this.context.dataSource = this.dataSource;
this.context.persistKey = this.persistKey;
this.context.isSortable = this.sortable === true;
this.context.isMultiSortable = this.multiSortable === true;
this.context.disableMultiSelect = this.disableMultiSelect === true;
+ this.context.rowFlairIsEnabled = this.rowFlairIsEnabled === true;
+
+ // TS doesn't seem to like: let foo = bar || () => '';
+ this.context.rowFlairCallback =
+ this.rowFlairCallback || function () { return ''; };
+ this.context.rowClassCallback =
+ his.rowClassCallback || function () { return ''; };
+ this.context.cellClassCallback =
+ this.cellClassCallback || function() { return ''; };
+
this.context.init();
}
toolbarActions: GridToolbarAction[];
lastSelectedIndex: any;
pageChanges: Subscription;
- mainLabel: string;
+ rowFlairIsEnabled: boolean;
+ rowFlairCallback: (row: any) => string;
+ rowClassCallback: (row: any) => string;
+ cellClassCallback: (row: any, col: GridColumn) => string;
// Services injected by our grid component
idl: IdlService;
// Give templates a chance to render before printing
setTimeout(() => {
- this.dispatchPrint(printReq)
+ this.dispatchPrint(printReq);
this.reset();
});
}
}
}
- // Clear the print data
+ // Clear the print data
reset() {
this.isPrinting = false;
this.template = null;
this.store.setLocalItem('eg.print.last_printed', {
content: printReq.text,
context: printReq.printContext,
- content_type: printReq.contentType,
+ content_type: printReq.contentType,
show_dialog: printReq.showDialog
});
@Component({
selector: 'eg-about',
- //styleUrls: ['about.component.css'],
templateUrl: 'about.component.html'
})
};
@Input() set recordId(recId: number) {
- this.recId = recId
+ this.recId = recId;
if (this.initDone) {
// Fire any record specific actions here
}
this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
// sorting not currently supported
return this.fetchCopies(pager);
- }
+ };
this.copyContext = {
holdable: (copy: any) => {
&& copy.location_holdable === 't'
&& copy.status_holdable === 't';
}
- }
+ };
}
collectData() {
pager.offset,
this.staffCat.prefOrg ? this.staffCat.prefOrg.id() : null
).pipe(map(copy => {
- copy.active_date = copy.active_date || copy.create_date
+ copy.active_date = copy.active_date || copy.create_date;
return copy;
}));
}
case 'ident': // identifier query input
const iq = this.searchContext.identQuery;
- const qt = this.searchContext.identQueryType
+ const qt = this.searchContext.identQueryType;
if (iq) {
// Ident queries ignore search-specific filters.
this.searchContext.reset();
}
break;
}
-
+
this.searchByForm();
}
<eg-grid #cbtGrid idlClass="cbt"
[dataSource]="btSource"
[rowClassCallback]="btGridRowClassCallback"
+ [rowFlairIsEnabled]="true"
+ [rowFlairCallback]="btGridRowFlairCallback"
[cellClassCallback]="btGridCellClassCallback"
[sortable]="true">
<eg-grid-column name="test" [cellTemplate]="cellTmpl"
}
}
+ btGridRowFlairCallback(row: any): string {
+ if (row.id() === 2) {
+ return 'priority_high';
+ } else if (row.id() === 3) {
+ return 'not_interested';
+ }
+ }
+
// apply to all 'name' columns regardless of row
btGridCellClassCallback(row: any, col: GridColumn): string {
if (col.name === 'name') {
+ if (row.id() === 7) {
+ return 'text-lowercase font-weight-bold text-info';
+ }
return 'text-uppercase font-weight-bold text-success';
}
}
item.bucket(id);
item.target_biblio_record_entry(this.recId);
this.net.request(
- 'open-ils.actor',
+ 'open-ils.actor',
'open-ils.actor.container.item.create',
this.auth.token(), 'biblio', item
).subscribe(resp => {
import {NetService} from '@eg/core/net.service';
interface NewVolumeData {
- owner: number,
- label?: string
+ owner: number;
+ label?: string;
}
@Injectable()