From 5fd5ccc77de64143985e135e2bdb71670dde73da Mon Sep 17 00:00:00 2001 From: Jane Sandberg Date: Thu, 2 Feb 2023 16:15:32 -0800 Subject: [PATCH] LP2002435: Add optional undelete action to basic admin page By default, this undelete action is only enabled on the shelving location editor. Also, only show admin page delete button if everything selected is deleteable Signed-off-by: Jane Sandberg Signed-off-by: Michele Morgan --- .../staff/admin/basic-admin-page.component.spec.ts | 113 +++++++++++++++++++++ .../app/staff/admin/basic-admin-page.component.ts | 10 +- .../src/app/staff/admin/local/routing.module.ts | 1 + .../share/admin-page/admin-page.component.html | 12 ++- .../share/admin-page/admin-page.component.spec.ts | 65 ++++++++++++ .../staff/share/admin-page/admin-page.component.ts | 50 +++++++++ 6 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.spec.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.spec.ts diff --git a/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.spec.ts b/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.spec.ts new file mode 100644 index 0000000000..764c473b4d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.spec.ts @@ -0,0 +1,113 @@ +import { Component, Input } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { IdlObject, IdlService } from '@eg/core/idl.service'; +import { of } from 'rxjs'; +import { BasicAdminPageComponent } from './basic-admin-page.component'; + +@Component({ + selector: 'eg-title', + template: '' +}) +class MockTitleComponent { + @Input() prefix: string; +} + +@Component({ + selector: 'eg-staff-banner', + template: '' +}) +class MockStaffBannerComponent { + @Input() bannerText: string; +} + +@Component({ + selector: 'eg-admin-page', + template: '' +}) +class MockAdminPageComponent { + @Input() configLinkBasePath: string; + @Input() defaultNewRecord: IdlObject; + @Input() disableOrgFilter: boolean; + @Input() fieldOrder: string; + @Input() idlClass: string; + @Input() persistKeyPfx: string; + @Input() readonlyFields: string; + @Input() enableUndelete: boolean; +} + +describe('Component: BasicAdminPage', () => { + let component: BasicAdminPageComponent; + let fixture: ComponentFixture; + let idlServiceStub: Partial; + let routeStub: any; + + beforeEach(() => { + idlServiceStub = { + create: (cls: string, seed?: []) => { + return { + a: seed || [], + classname: cls, + _isfieldmapper: true, + + field1(value: any): any { + this.a[0] = value; + return this.a[0]; + } + }; + }, + classes: [{ tbl1: { table: 'schema1.table1' } }] + }; + + const emptyParamMap: ParamMap = { + has: (name: string) => false, + get: (name: string) => null, + getAll: (name: string) => [], + keys: [] + }; + const data = [{ + schema: 'schema1', + table: 'table1', + defaultNewRecord: { field1: 'value1' }, + enableUndelete: true + }]; + const parentRoute = { url: of('') }; + const snapshot = { parent: { url: [{ path: '' }] } }; + routeStub = { + paramMap: of(emptyParamMap), + data: of(data), + parent: parentRoute, + snapshot + }; + + TestBed.configureTestingModule({ + imports: [], + providers: [ + { provide: IdlService, useValue: idlServiceStub }, + { provide: ActivatedRoute, useValue: routeStub } + ], + declarations: [ + BasicAdminPageComponent, + MockTitleComponent, + MockStaffBannerComponent, + MockAdminPageComponent + ] + }); + fixture = TestBed.createComponent(BasicAdminPageComponent); + component = fixture.componentInstance; + component.idlClass = 'tbl1'; + fixture.detectChanges(); + }); + + it('sets default new record from routing data', () => { + const adminPage: MockAdminPageComponent = fixture.debugElement.query( + By.directive(MockAdminPageComponent)).componentInstance; + expect(adminPage.defaultNewRecord.a[0]).toEqual('value1'); + }); + it('sets enableUndelete from routing data', () => { + const adminPage: MockAdminPageComponent = fixture.debugElement.query( + By.directive(MockAdminPageComponent)).componentInstance; + expect(adminPage.enableUndelete).toEqual(true); + }); +}); diff --git a/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts index 91b2040a34..a6d81d2395 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts @@ -19,6 +19,7 @@ import {tap, switchMap} from 'rxjs/operators'; configLinkBasePath="{{configLinkBasePath}}" fieldOrder="{{fieldOrder}}" readonlyFields="{{readonlyFields}}" + [enableUndelete]="enableUndelete" [disableOrgFilter]="disableOrgFilter"> ` @@ -36,6 +37,8 @@ export class BasicAdminPageComponent implements OnInit { // Tell the admin page to disable and hide the automagic org unit filter disableOrgFilter: boolean; + enableUndelete: boolean; + private getParams$: Observable; private getRouteData$: Observable; private getParentUrl$: Observable; @@ -69,9 +72,10 @@ export class BasicAdminPageComponent implements OnInit { if (!this.table) { this.table = data['table']; } - this.disableOrgFilter = data['disableOrgFilter']; - this.fieldOrder = data['fieldOrder']; - this.readonlyFields = data['readonlyFields']; + this.disableOrgFilter = data['disableOrgFilter']; + this.enableUndelete = data['enableUndelete']; + this.fieldOrder = data['fieldOrder']; + this.readonlyFields = data['readonlyFields']; } })); 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 5267c04e90..b78565ffa2 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 @@ -28,6 +28,7 @@ const routes: Routes = [{ data: [{ schema: 'asset', table: 'copy_location', + enableUndelete: true, readonlyFields: 'deleted', fieldOrder: 'owning_lib,name,opac_visible,circulate,holdable,hold_verify,checkin_alert,deleted,label_prefix,label_suffix,url,id'}] }, { diff --git a/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html index 58b940be97..82d570bf2b 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html +++ b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html @@ -10,6 +10,12 @@ {{idlClassDef.label}} Successfully Deleted +Undelete of {{idlClassDef.label}} failed or was not allowed + + +{{idlClassDef.label}} Successfully undeleted + + {{idlClassDef.label}} Successfully Created @@ -64,7 +70,11 @@ - + + + diff --git a/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.spec.ts b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.spec.ts new file mode 100644 index 0000000000..28139b5ce5 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.spec.ts @@ -0,0 +1,65 @@ +import { ActivatedRoute } from "@angular/router"; +import { AdminPageComponent } from "./admin-page.component"; +import { Location } from '@angular/common'; +import { FormatService } from "@eg/core/format.service"; +import { IdlService } from "@eg/core/idl.service"; +import { OrgService } from "@eg/core/org.service"; +import { AuthService } from "@eg/core/auth.service"; +import { PcrudService } from "@eg/core/pcrud.service"; +import { PermService } from "@eg/core/perm.service"; +import { ToastService } from "@eg/share/toast/toast.service"; + +describe('CopyAttrsComponent', () => { + let component: AdminPageComponent; + + const routeMock = jasmine.createSpyObj(['snapshot']); + const locationMock = jasmine.createSpyObj(['prepareExternalUrl']); + const formatMock = jasmine.createSpyObj(['transform']); + const idlMock = jasmine.createSpyObj(['classes']); + const orgMock = jasmine.createSpyObj(['get']); + const authMock = jasmine.createSpyObj(['user']); + const pcrudMock = jasmine.createSpyObj(['retrieveAll']); + const permMock = jasmine.createSpyObj(['hasWorkPermAt']); + const toastMock = jasmine.createSpyObj(['success']); + beforeEach(() => { + component = new AdminPageComponent(routeMock, locationMock, formatMock, + idlMock, orgMock, authMock, pcrudMock, permMock, toastMock); + }) + + describe('#shouldDisableDelete', () => { + it('returns true if one of the rows is already deleted', () => { + const rows = [ + {isdeleted: () => true, a: [], classname: '', _isfieldmapper: true }, + {isdeleted: () => false, a: [], classname: '', _isfieldmapper: true } + ]; + expect(component.shouldDisableDelete(rows)).toBe(true); + }); + it('returns true if no rows selected', () => { + expect(component.shouldDisableDelete([])).toBe(true); + }) + it('returns false (i.e. you _should_ display delete) if no selected rows are deleted', () => { + const rows = [ + {isdeleted: () => false, deleted: () => 'f', a: [], classname: '', _isfieldmapper: true } + ]; + expect(component.shouldDisableDelete(rows)).toBe(false); + }) + }); + describe('#shouldDisableUndelete', () => { + it('returns true if none of the rows are deleted', () => { + const rows = [ + {isdeleted: () => false, a: [], classname: '', _isfieldmapper: true }, + {deleted: () => 'f', a: [], classname: '', _isfieldmapper: true } + ]; + expect(component.shouldDisableUndelete(rows)).toBe(true); + }); + it('returns true if no rows selected', () => { + expect(component.shouldDisableUndelete([])).toBe(true); + }) + it('returns false (i.e. you _should_ display undelete) if all selected rows are deleted', () => { + const rows = [ + {deleted: () => 't', a: [], classname: '', _isfieldmapper: true } + ]; + expect(component.shouldDisableUndelete(rows)).toBe(false); + }) + }); +}); diff --git a/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts index 7098c70f2d..d9de7bde3d 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts @@ -60,6 +60,9 @@ export class AdminPageComponent implements OnInit { // Disable the auto-matic org unit field filter @Input() disableOrgFilter: boolean; + // Give the grid an option to undelete any deleted rows + @Input() enableUndelete: boolean; + // Include objects linking to org units which are ancestors // of the selected org unit. @Input() includeOrgAncestors: boolean; @@ -106,6 +109,8 @@ export class AdminPageComponent implements OnInit { @ViewChild('updateFailedString', { static: true }) updateFailedString: StringComponent; @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent; @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent; + @ViewChild('undeleteFailedString', { static: true }) undeleteFailedString: StringComponent; + @ViewChild('undeleteSuccessString', { static: true }) undeleteSuccessString: StringComponent; @ViewChild('translator', { static: true }) translator: TranslateComponent; idlClassDef: any; @@ -340,6 +345,21 @@ export class AdminPageComponent implements OnInit { editOneThing(idlThings.shift()); } + undeleteSelected(idlThings: IdlObject[]) { + idlThings.forEach(idlThing => idlThing.deleted(false)); + this.pcrud.update(idlThings).subscribe( + val => { + this.undeleteSuccessString.current() + .then(str => this.toast.success(str)); + }, + err => { + this.undeleteFailedString.current() + .then(str => this.toast.danger(str)); + }, + () => this.grid.reload() + ); + } + deleteSelected(idlThings: IdlObject[]) { idlThings.forEach(idlThing => idlThing.isdeleted(true)); this.pcrud.autoApply(idlThings).subscribe( @@ -355,6 +375,36 @@ export class AdminPageComponent implements OnInit { ); } + shouldDisableDelete(rows: IdlObject[]): boolean { + if (rows.length === 0) { + return true; + } else { + const deletedRows = rows.filter((row) => { + if (row.deleted && row.deleted() === 't') { + return true; + } else if (row.isdeleted) { + return row.isdeleted(); + } + }); + return deletedRows.length > 0; + } + } + + shouldDisableUndelete(rows: IdlObject[]): boolean { + if (rows.length === 0) { + return true; + } else { + const deletedRows = rows.filter((row) => { + if (row.deleted && row.deleted() === 't') { + return true; + } else if (row.isdeleted) { + return row.isdeleted(); + } + }); + return deletedRows.length !== rows.length; + } + } + createNew() { this.editDialog.mode = 'create'; // We reuse the same editor for all actions. Be sure -- 2.11.0