--- /dev/null
+<div class="input-group">
+ <div class="input-group-prepend">
+ <span class="input-group-text">{{labelText}}</span>
+ </div>
+ <eg-org-select [domId]="domId"
+ (onChange)="orgOnChange($event)"
+ [limitPerms]="limitPerms"
+ [initialOrgId]="selectedOrgId">
+ </eg-org-select>
+</div>
+<form class="pl-2" [formGroup]="familySelectors">
+ <div class="form-check" *ngIf="!hideAncestorSelector">
+ <input type="checkbox"
+ formControlName="includeAncestors"
+ (blur)="registerOnTouched()"
+ class="form-check-input" id="{{domId}}-include-ancestors">
+ <label class="form-check-label" for="{{domId}}-include-ancestors" i18n>+ Ancestors</label>
+ </div>
+ <div class="form-check" *ngIf="!hideDescendantSelector">
+ <input type="checkbox"
+ formControlName="includeDescendants"
+ (blur)="registerOnTouched()"
+ class="form-check-input" id="{{domId}}-include-descendants">
+ <label class="form-check-label" for="{{domId}}-include-descendants" i18n>+ Descendants</label>
+ </div>
+</form>
--- /dev/null
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {Component, DebugElement, Input} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {OrgFamilySelectComponent} from './org-family-select.component';
+import {ReactiveFormsModule} from '@angular/forms';
+import {CookieService} from 'ngx-cookie';
+import {OrgService} from '@eg/core/org.service';
+
+@Component({
+ selector: 'eg-org-select',
+ template: ''
+})
+class MockOrgSelectComponent {
+ @Input() domId: string;
+ @Input() limitPerms: string;
+ @Input() initialOrgId: number;
+}
+
+describe('Component: OrgFamilySelect', () => {
+ let component: OrgFamilySelectComponent;
+ let fixture: ComponentFixture<OrgFamilySelectComponent>;
+ let includeAncestors: DebugElement;
+ let includeDescendants: DebugElement;
+ let orgServiceStub: Partial<OrgService>;
+ let cookieServiceStub: Partial<CookieService>;
+
+ beforeEach(() => {
+ // stub of OrgService for testing
+ // with a very simple org structure:
+ // 1 is the root note
+ // 2 is its child
+ orgServiceStub = {
+ root: () => {
+ return {
+ a: [],
+ classname: 'aou',
+ _isfieldmapper: true,
+ id: () => 1};
+ },
+ get: (ouId: number) => {
+ return {
+ a: [],
+ classname: 'aou',
+ _isfieldmapper: true,
+ children: () => Array(2 - ouId) };
+ }
+ };
+ cookieServiceStub = {};
+ TestBed.configureTestingModule({
+ imports: [
+ ReactiveFormsModule,
+ ], providers: [
+ { provide: CookieService, useValue: cookieServiceStub },
+ { provide: OrgService, useValue: orgServiceStub},
+ ], declarations: [
+ OrgFamilySelectComponent,
+ MockOrgSelectComponent,
+ ]});
+ fixture = TestBed.createComponent(OrgFamilySelectComponent);
+ component = fixture.componentInstance;
+ component.domId = 'family-test';
+ fixture.detectChanges();
+ });
+
+
+ it('provides includeAncestors checkbox by default', () => {
+ fixture.whenStable().then(() => {
+ includeAncestors = fixture.debugElement.query(By.css('#family-test-include-ancestors'));
+ expect(includeAncestors.nativeElement).toBeTruthy();
+ });
+ });
+
+ it('provides includeDescendants checkbox by default', () => {
+ fixture.whenStable().then(() => {
+ includeDescendants = fixture.debugElement.query(By.css('#family-test-include-descendants'));
+ expect(includeDescendants.nativeElement).toBeTruthy();
+ });
+ });
+
+ it('allows user to turn off includeAncestors checkbox', () => {
+ fixture.whenStable().then(() => {
+ component.hideAncestorSelector = true;
+ fixture.detectChanges();
+ includeAncestors = fixture.debugElement.query(By.css('#family-test-include-ancestors'));
+ expect(includeAncestors).toBeNull();
+ });
+ });
+
+ it('allows user to turn off includeDescendants checkbox', () => {
+ fixture.whenStable().then(() => {
+ component.hideDescendantSelector = true;
+ fixture.detectChanges();
+ includeDescendants = fixture.debugElement.query(By.css('#family-test-include-descendants'));
+ expect(includeDescendants).toBeNull();
+ });
+ });
+
+ it('disables includeAncestors checkbox when root OU is chosen', () => {
+ fixture.whenStable().then(() => {
+ component.selectedOrgId = 1;
+ fixture.detectChanges();
+ includeAncestors = fixture.debugElement.query(By.css('#family-test-include-ancestors'));
+ expect(includeAncestors.nativeElement.disabled).toBe(true);
+ });
+ });
+
+});
+
--- /dev/null
+import {Component, EventEmitter, OnInit, Input, Output, ViewChild, forwardRef} from '@angular/core';
+import {ControlValueAccessor, FormGroup, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {AuthService} from '@eg/core/auth.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+
+export interface OrgFamily {
+ primaryOrgId: number;
+ includeAncestors?: boolean;
+ includeDescendants?: boolean;
+ orgIds?: number[];
+}
+
+@Component({
+ selector: 'eg-org-family-select',
+ templateUrl: 'org-family-select.component.html',
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => OrgFamilySelectComponent),
+ multi: true
+ }
+ ]
+})
+export class OrgFamilySelectComponent implements ControlValueAccessor, OnInit {
+
+ // The label for this input
+ @Input() labelText = 'Library';
+
+ // Should the Ancestors checkbox be hidden?
+ @Input() hideAncestorSelector = false;
+
+ // Should the Descendants checkbox be hidden?
+ @Input() hideDescendantSelector = false;
+
+ // Should the Ancestors checkbox be checked by default?
+ //
+ // Ignored if [hideAncestorSelector]="true"
+ @Input() ancestorSelectorChecked = false;
+
+ // Should the Descendants checkbox be checked by default?
+ //
+ // Ignored if [hideDescendantSelector]="true"
+ @Input() descendantSelectorChecked = false;
+
+ // Default org unit
+ @Input() selectedOrgId: number;
+
+ // Only show the OUs that the user has certain permissions at
+ @Input() limitPerms: string[];
+
+ @Input() domId: string;
+
+ // this is the most up-to-date value used for ngModel and reactive form
+ // subscriptions
+ options: OrgFamily;
+
+ orgOnChange: ($event: IdlObject) => void;
+ emitArray: () => void;
+
+ familySelectors: FormGroup;
+
+ propagateChange = (_: OrgFamily) => {};
+
+ constructor(
+ private auth: AuthService,
+ private org: OrgService
+ ) {
+ }
+
+ ngOnInit() {
+ if (this.selectedOrgId) {
+ this.options = {primaryOrgId: this.selectedOrgId};
+ } else if (this.auth.user()) {
+ this.options = {primaryOrgId: this.auth.user().ws_ou()};
+ }
+
+ this.familySelectors = new FormGroup({
+ 'includeAncestors': new FormControl({
+ value: this.ancestorSelectorChecked,
+ disabled: this.disableAncestorSelector()}),
+ 'includeDescendants': new FormControl({
+ value: this.descendantSelectorChecked,
+ disabled: this.disableDescendantSelector()}),
+ });
+
+ if (!this.domId) {
+ this.domId = 'org-family-select-' + Math.floor(Math.random() * 100000);
+ }
+
+ this.familySelectors.valueChanges.subscribe(val => {
+ this.emitArray();
+ });
+
+ this.orgOnChange = ($event: IdlObject) => {
+ this.options.primaryOrgId = $event.id();
+ this.disableAncestorSelector() ? this.includeAncestors.disable() : this.includeAncestors.enable();
+ this.disableDescendantSelector() ? this.includeDescendants.disable() : this.includeDescendants.enable();
+ this.emitArray();
+ };
+
+ this.emitArray = () => {
+ // Prepare and emit an array containing the primary org id and
+ // optionally ancestor and descendant org units.
+
+ this.options.orgIds = [this.options.primaryOrgId];
+
+ if (this.includeAncestors.value) {
+ this.options.orgIds = this.org.ancestors(this.options.primaryOrgId, true);
+ }
+
+ if (this.includeDescendants.value) {
+ // can result in duplicate workstation org IDs... meh
+ this.options.orgIds = this.options.orgIds.concat(
+ this.org.descendants(this.options.primaryOrgId, true));
+ }
+
+ this.propagateChange(this.options);
+ };
+
+ }
+
+ writeValue(value: OrgFamily) {
+ if (value) {
+ this.selectedOrgId = value['primaryOrgId'];
+ this.familySelectors.patchValue({
+ 'includeAncestors': value['includeAncestors'] ? value['includeAncestors'] : false,
+ 'includeDescendants': value['includeDescendants'] ? value['includeDescendants'] : false,
+ });
+ }
+ }
+
+ registerOnChange(fn) {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched() {}
+
+ disableAncestorSelector(): boolean {
+ return this.options.primaryOrgId === this.org.root().id();
+ }
+
+ disableDescendantSelector(): boolean {
+ const contextOrg = this.org.get(this.options.primaryOrgId);
+ return contextOrg.children().length === 0;
+ }
+
+ get includeAncestors() {
+ return this.familySelectors.get('includeAncestors');
+ }
+ get includeDescendants() {
+ return this.familySelectors.get('includeDescendants');
+ }
+
+}
+
import {ComboboxComponent} from '@eg/share/combobox/combobox.component';
import {ComboboxEntryComponent} from '@eg/share/combobox/combobox-entry.component';
import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
+import {OrgFamilySelectComponent} from '@eg/share/org-family-select/org-family-select.component';
import {AccessKeyDirective} from '@eg/share/accesskey/accesskey.directive';
import {AccessKeyService} from '@eg/share/accesskey/accesskey.service';
import {AccessKeyInfoComponent} from '@eg/share/accesskey/accesskey-info.component';
import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.component';
import {EgHelpPopoverComponent} from '@eg/share/eg-help-popover/eg-help-popover.component';
+import {ReactiveFormsModule} from '@angular/forms';
/**
* Imports the EG common modules and adds modules common to all staff UI's.
ComboboxComponent,
ComboboxEntryComponent,
OrgSelectComponent,
+ OrgFamilySelectComponent,
AccessKeyDirective,
AccessKeyInfoComponent,
ToastComponent,
],
imports: [
EgCommonModule,
- GridModule
+ GridModule,
+ ReactiveFormsModule
],
exports: [
EgCommonModule,
ComboboxComponent,
ComboboxEntryComponent,
OrgSelectComponent,
+ OrgFamilySelectComponent,
AccessKeyDirective,
AccessKeyInfoComponent,
ToastComponent,
<h4>PCRUD auto flesh and FormatService detection</h4>
<div *ngIf="aMetarecord">Fingerprint: {{aMetarecord}}</div>
+<div class="row">
+ <div class="card col-md-6">
+ <div class="card-body">
+ <h3 class="card-title">Do you like template-driven forms?</h3>
+ <div class="card-text">
+ <eg-org-family-select
+ [ancestorSelectorChecked]="true"
+ [hideDescendantSelector]="true"
+ selectedOrgId="7"
+ labelText="Choose the best libraries"
+ ngModel #bestOnes="ngModel">
+ </eg-org-family-select>
+ The best libraries are: {{bestOnes.value | json}}
+ </div>
+ </div>
+ </div>
+ <form class="card col-md-6" [formGroup]="badOrgForm">
+ <div class="card-body">
+ <h3 class="card-title">Or perhaps reactive forms interest you?</h3>
+ <div class="card-text">
+ <eg-org-family-select
+ formControlName="badOrgSelector"
+ labelText="Choose the worst libraries">
+ </eg-org-family-select>
+ <div *ngIf="!badOrgForm.valid" class="alert alert-danger">
+ <span class="material-icons">error</span>
+ <span i18n>Too many bad libraries!</span>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
import {FormatService} from '@eg/core/format.service';
import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {FormGroup, FormControl} from '@angular/forms';
@Component({
templateUrl: 'sandbox.component.html'
dynamicTitleText: string;
+ badOrgForm: FormGroup;
+
complimentEvergreen: (rows: IdlObject[]) => void;
notOneSelectedRow: (rows: IdlObject[]) => boolean;
}
ngOnInit() {
+ this.badOrgForm = new FormGroup({
+ 'badOrgSelector': new FormControl(
+ {'id': 4, 'includeAncestors': false, 'includeDescendants': true}, (c: FormControl) => {
+ // An Angular custom validator
+ if (c.value.orgIds && c.value.orgIds.length > 5) {
+ return { tooMany: 'That\'s too many bad libraries!' };
+ } else {
+ return null;
+ }
+ } )
+ });
+
+ this.badOrgForm.get('badOrgSelector').valueChanges.subscribe(bad => {
+ this.toast.danger('The worst libraries are: ' + JSON.stringify(bad.orgIds));
+ });
this.gridDataSource.data = [
{name: 'Jane', state: 'AZ'},
import {StaffCommonModule} from '@eg/staff/common.module';
import {SandboxRoutingModule} from './routing.module';
import {SandboxComponent} from './sandbox.component';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
@NgModule({
declarations: [
imports: [
StaffCommonModule,
SandboxRoutingModule,
+ FormsModule,
+ ReactiveFormsModule
],
providers: [
]
<eg-string #createErrString [template]="createErrStrTmpl"></eg-string>
<ng-container *ngIf="orgField">
- <div class="d-flex">
- <div>
- <div class="input-group">
- <div class="input-group-prepend">
- <span class="input-group-text">{{orgFieldLabel}}</span>
- </div>
- <eg-org-select
- [limitPerms]="viewPerms"
- [initialOrg]="contextOrg"
- (onChange)="orgOnChange($event)">
- </eg-org-select>
- </div>
- </div>
- <div class="pl-2">
- <div class="form-check">
- <input type="checkbox" (click)="grid.reload()"
- [disabled]="disableAncestorSelector()"
- [(ngModel)]="includeOrgAncestors"
- class="form-check-input" id="include-ancestors">
- <label class="form-check-label" for="include-ancestors" i18n>+ Ancestors</label>
- </div>
- <div class="form-check">
- <input type="checkbox" (click)="grid.reload()"
- [disabled]="disableDescendantSelector()"
- [(ngModel)]="includeOrgDescendants"
- class="form-check-input" id="include-descendants">
- <label class="form-check-label" for="include-descendants" i18n>+ Descendants</label>
- </div>
- </div>
- </div>
+ <eg-org-family-select
+ [limitPerms]="viewPerms"
+ [selectedOrgId]="contextOrg.id()"
+ [(ngModel)]="searchOrgs"
+ (ngModelChange)="grid.reload()">
+ </eg-org-family-select>
<hr/>
</ng-container>
import {AuthService} from '@eg/core/auth.service';
import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
import {StringComponent} from '@eg/share/string/string.component';
+import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component';
/**
* General purpose CRUD interface for IDL objects
translatableFields: string[];
contextOrg: IdlObject;
+ searchOrgs: OrgFamily;
orgFieldLabel: string;
viewPerms: string;
canCreate: boolean;
if (this.orgField) {
this.orgFieldLabel = this.idlClassDef.field_map[this.orgField].label;
this.contextOrg = this.org.get(orgId) || this.org.root();
+ this.searchOrgs = {primaryOrgId: this.contextOrg.id()};
}
}
});
}
- orgOnChange(org: IdlObject) {
- this.contextOrg = org;
- this.grid.reload();
- }
-
initDataSource() {
this.dataSource = new GridDataSource();
const search: any = {};
- if (this.contextOrg) {
- // Filter rows by those linking to the context org and
- // optionally ancestor and descendant org units.
-
- let orgs = [this.contextOrg.id()];
-
- if (this.includeOrgAncestors) {
- orgs = this.org.ancestors(this.contextOrg, true);
- }
-
- if (this.includeOrgDescendants) {
- // can result in duplicate workstation org IDs... meh
- orgs = orgs.concat(
- this.org.descendants(this.contextOrg, true));
- }
-
- search[this.orgField] = orgs;
- }
+ search[this.orgField] = this.searchOrgs.orgIds || [this.contextOrg.id()];
if (this.gridFilters) {
// Lay the URL grid filters over our search object.
};
}
- disableAncestorSelector(): boolean {
- return this.contextOrg &&
- this.contextOrg.id() === this.org.root().id();
- }
-
- disableDescendantSelector(): boolean {
- return this.contextOrg && this.contextOrg.children().length === 0;
- }
-
showEditDialog(idlThing: IdlObject): Promise<any> {
this.editDialog.mode = 'update';
this.editDialog.recId = idlThing[this.pkeyField]();