From 36a39ad2c720c930139f07214094496593be78ff Mon Sep 17 00:00:00 2001 From: Mike Risher <mrisher@catalyte.io> Date: Tue, 20 Aug 2019 20:29:23 +0000 Subject: [PATCH] LP#1840327: port standing penalty admin interface to Angular Convert standing penalty types admin UI from DOJO to Angular. Name field is read only if the ID is below 100. Doing this involved: - creating a new standing penalty component - using rowFlairCallback functionality in the grid, so that an icon and tooltip is shown for fields where the name cannot be edited - making the ID show up in red text when it is below 100 - adding "readonly Override" functionality to fm-editor, so that some fields of a given type are read only, but others are not Signed-off-by: Mike Risher <mrisher@catalyte.io> Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org> --- .../src/app/share/fm-editor/fm-editor.component.ts | 19 ++- .../admin/local/admin-local-splash.component.html | 2 +- .../app/staff/admin/local/admin-local.module.ts | 4 +- .../src/app/staff/admin/local/routing.module.ts | 4 + .../admin/local/standing-penalty.component.html | 28 ++++ .../admin/local/standing-penalty.component.ts | 160 +++++++++++++++++++++ 6 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/local/standing-penalty.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/local/standing-penalty.component.ts diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts index a4fc62ba73..e5ed4006ba 100644 --- a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts +++ b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts @@ -67,6 +67,11 @@ export interface FmFieldOptions { // This only has an affect if the value is true. isReadonly?: boolean; + // If this function is defined, the function will be called + // at render time to see if the field should be marked readonly. + // This supersedes all other isReadonly specifiers. + isReadonlyOverride?: (field: string, record: IdlObject) => boolean; + // Render the field using this custom template instead of chosing // from the default set of form inputs. customTemplate?: CustomFieldTemplate; @@ -455,10 +460,16 @@ export class FmRecordEditorComponent let promise = null; const fieldOptions = this.fieldOptions[field.name] || {}; - - field.readOnly = this.mode === 'view' - || fieldOptions.isReadonly === true - || this.readonlyFieldsList.includes(field.name); + + if (this.mode === 'view') { + field.readOnly = true; + } else if (fieldOptions.isReadonlyOverride) { + field.readOnly = + !fieldOptions.isReadonlyOverride(field.name, this.record); + } else { + field.readOnly = fieldOptions.isReadonly === true + || this.readonlyFieldsList.includes(field.name); + } if (fieldOptions.isRequiredOverride) { field.isRequired = () => { diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html index 870deeb8dc..02565dfafa 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html @@ -55,7 +55,7 @@ <eg-link-table-link i18n-label label="Search Filter Groups" url="/eg/staff/admin/local/actor/search_filter_group"></eg-link-table-link> <eg-link-table-link i18n-label label="Standing Penalties" - url="/eg/staff/admin/local/config/standing_penalty"></eg-link-table-link> + routerLink="/staff/admin/local/config/standing_penalty"></eg-link-table-link> <eg-link-table-link i18n-label label="Statistical Categories Editor" url="/eg/staff/admin/local/asset/stat_cat_editor"></eg-link-table-link> <eg-link-table-link i18n-label label="Statistical Popularity Badges" diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local.module.ts index 7c8a6fa7cb..9f70ab78d6 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local.module.ts @@ -6,12 +6,14 @@ 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'; +import {StandingPenaltyComponent} from './standing-penalty.component'; @NgModule({ declarations: [ AdminLocalSplashComponent, AddressAlertComponent, - AdminCarouselComponent + AdminCarouselComponent, + StandingPenaltyComponent ], imports: [ AdminCommonModule, diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts index 4eda5855c4..39c6be7179 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts @@ -4,6 +4,7 @@ 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'; +import {StandingPenaltyComponent} from './standing-penalty.component'; const routes: Routes = [{ path: 'splash', @@ -19,6 +20,9 @@ const routes: Routes = [{ path: 'container/carousel', component: AdminCarouselComponent }, { + path: 'config/standing_penalty', + component: StandingPenaltyComponent +}, { path: ':schema/:table', component: BasicAdminPageComponent }]; diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/standing-penalty.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/standing-penalty.component.html new file mode 100644 index 0000000000..351d1cbda5 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/standing-penalty.component.html @@ -0,0 +1,28 @@ +<eg-title i18n-prefix prefix="Standing Penalty Administration"></eg-title> +<eg-staff-banner bannerText="Standing Penalty Types" i18n-bannerText> +</eg-staff-banner> + +<eg-string #cspFlairTooltip i18n-text text="Limited Editing"></eg-string> +<div class="w-100 mt-2 mb-2"> + <eg-grid idlClass="csp" + [dataSource]="cspSource" + [rowFlairIsEnabled]="true" + [rowFlairCallback]="cspRowFlairCallback" + [cellClassCallback]="cspGridCellClassCallback" + [sortable]="true"> + <eg-grid-toolbar-button + label="New Standing Penalty Type" i18n-label (onClick)="createNew()"> + </eg-grid-toolbar-button> + <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelected($event)"> + </eg-grid-toolbar-action> + <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteSelected($event)"> + </eg-grid-toolbar-action> + </eg-grid> +</div> + +<eg-fm-record-editor #editDialog + idlClass="csp" + [fieldOptions]="{name: {isReadonlyOverride: cspReadonlyOverride}}" + [preloadLinkedValues]="true" + readonlyFields="name"> +</eg-fm-record-editor> diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/standing-penalty.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/standing-penalty.component.ts new file mode 100644 index 0000000000..f1c4bd582f --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/standing-penalty.component.ts @@ -0,0 +1,160 @@ +import {Pager} from '@eg/share/util/pager'; +import {Component, OnInit, Input, ViewChild} from '@angular/core'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridColumn, GridRowFlairEntry} from '@eg/share/grid/grid'; +import {IdlObject} from '@eg/core/idl.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; + +@Component({ + templateUrl: './standing-penalty.component.html' +}) + +export class StandingPenaltyComponent implements OnInit { + recId: number; + gridDataSource: GridDataSource; + initDone = false; + cspSource: GridDataSource = new GridDataSource(); + @ViewChild('partsGrid') partsGrid: GridComponent; + @ViewChild('editDialog') editDialog: FmRecordEditorComponent; + @ViewChild('grid') grid: GridComponent; + @ViewChild('successString') successString: StringComponent; + @ViewChild('createString') createString: StringComponent; + @ViewChild('createErrString') createErrString: StringComponent; + @ViewChild('updateFailedString') updateFailedString: StringComponent; + @ViewChild('cspFlairTooltip') private cspFlairTooltip: StringComponent; + + cspRowFlairCallback: (row: any) => GridRowFlairEntry; + + canCreate: boolean; + canDelete: boolean; + deleteSelected: (rows: IdlObject[]) => void; + + permissions: {[name: string]: boolean}; + + // Default sort field, used when no grid sorting is applied. + @Input() sortField: string; + + @Input() idlClass: string = "csp"; + // Size of create/edito dialog. Uses large by default. + @Input() dialogSize: 'sm' | 'lg' = 'lg'; + // Optional comma-separated list of read-only fields + // @Input() readonlyFields: string; + + @Input() set recordId(id: number) { + this.recId = id; + // Only force new data collection when recordId() + // is invoked after ngInit() has already run. + if (this.initDone) { + this.partsGrid.reload(); + } + } + + constructor( + private pcrud: PcrudService, + private toast: ToastService + ) { + this.gridDataSource = new GridDataSource(); + } + + ngOnInit() { + this.initDone = true; + this.cspSource.getRows = (pager: Pager, sort: any[]) => { + const orderBy: any = {}; + if (sort.length) { + // Sort specified from grid + orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir; + } else if (this.sortField) { + // Default sort field + orderBy[this.idlClass] = this.sortField; + } + + const searchOps = { + offset: pager.offset, + limit: pager.limit, + order_by: orderBy + }; + return this.pcrud.retrieveAll('csp', searchOps, {fleshSelectors: true}); + } + + this.cspRowFlairCallback = (row: any): GridRowFlairEntry => { + const flair = {icon: null, title: null}; + if (row.id() < 100) { + flair.icon = 'not_interested'; + flair.title = this.cspFlairTooltip.text; + } + return flair; + } + } + + cspReadonlyOverride = (field: string, copy: IdlObject): boolean => { + if (copy.id() >= 100) { + return true; + } + return false; + } + + cspGridCellClassCallback = (row: any, col: GridColumn): string => { + if (col.name === "id" && row.a[0] < 100) { + return "text-danger"; + } + return ""; + }; + + showEditDialog(standingPenalty: IdlObject): Promise<any> { + this.editDialog.mode = 'update'; + this.editDialog.recId = standingPenalty["id"](); + return new Promise((resolve, reject) => { + this.editDialog.open({size: this.dialogSize}).subscribe( + result => { + this.successString.current() + .then(str => this.toast.success(str)); + this.grid.reload(); + resolve(result); + }, + error => { + this.updateFailedString.current() + .then(str => this.toast.danger(str)); + reject(error); + } + ); + }); + } + + editSelected(standingPenaltyFields: IdlObject[]) { + // Edit each IDL thing one at a time + const editOneThing = (standingPenalty: IdlObject) => { + if (!standingPenalty) { return; } + + this.showEditDialog(standingPenalty).then( + () => editOneThing(standingPenaltyFields.shift())); + }; + + editOneThing(standingPenaltyFields.shift()); + } + + createNew() { + this.editDialog.mode = 'create'; + // We reuse the same editor for all actions. Be sure + // create action does not try to modify an existing record. + this.editDialog.recId = null; + this.editDialog.record = null; + this.editDialog.open({size: this.dialogSize}).subscribe( + ok => { + this.createString.current() + .then(str => this.toast.success(str)); + this.grid.reload(); + }, + rejection => { + if (!rejection.dismissed) { + this.createErrString.current() + .then(str => this.toast.danger(str)); + } + } + ); + } + +} + -- 2.11.0