private orgMap: {[id: number]: IdlObject} = {};
private settingsCache: OrgSettingsBatch = {};
+ private orgTypeMap: {[id: number]: IdlObject} = {};
+ private orgTypeList: IdlObject[] = [];
+
constructor(
private net: NetService,
private auth: AuthService,
return this.orgList;
}
- // Returns a list of org unit type objects
typeList(): IdlObject[] {
- const types = [];
- this.list().forEach(org => {
- if ((types.filter(t => t.id() === org.ou_type().id())).length === 0) {
- types.push(org.ou_type());
- }
- });
- return types;
+ return this.orgTypeList;
+ }
+
+ typeMap(): {[id: number]: IdlObject} {
+ return this.orgTypeMap;
}
/**
node = this.orgTree;
this.orgMap = {};
this.orgList = [];
+ this.orgTypeMap = {};
}
this.orgMap[node.id()] = node;
this.orgList.push(node);
+
+ this.orgTypeMap[node.ou_type().id()] = node.ou_type();
+ if (!this.orgTypeList.filter(t => t.id() === node.ou_type().id())[0]) {
+ this.orgTypeList.push(node.ou_type());
+ }
+
node.children().forEach(c => this.absorbTree(c));
}
<eg-link-table-link i18n-label label="Org Unit Setting Types"
routerLink="/staff/admin/server/config/org_unit_setting_type"></eg-link-table-link>
<eg-link-table-link i18n-label label="Organizational Units"
- url="/eg/staff/admin/server/legacy/actor/org_unit"></eg-link-table-link>
+ routerLink="/staff/admin/server/actor/org_unit"></eg-link-table-link>
<eg-link-table-link i18n-label label="Permission Groups"
routerLink="/staff/admin/server/permission/grp_tree"></eg-link-table-link>
<eg-link-table-link i18n-label label="Permissions"
import {NgModule} from '@angular/core';
import {TreeModule} from '@eg/share/tree/tree.module';
-import {AdminServerRoutingModule} from './routing.module';
import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {AdminServerRoutingModule} from './routing.module';
import {AdminServerSplashComponent} from './admin-server-splash.component';
import {OrgUnitTypeComponent} from './org-unit-type.component';
import {PrintTemplateComponent} from './print-template.component';
--- /dev/null
+
+<ngb-tabset #addressTabs *ngIf="orgUnit" (tabChange)="tabChanged($event)">
+ <ng-container *ngFor="let type of addrTypes()">
+ <b>type = {{type}}</b>
+
+ <ngb-tab *ngIf="addr(type)"
+ i18n-title id="{{type}}"
+ title="{{type === 'billing_address' ? 'Physical Address' :
+ (type === 'holds_address' ? 'Holds Address' :
+ (type === 'mailing_address' ? 'Mailing Address' : 'ILL Address'))}}">
+
+ <ng-template ngbTabContent>
+ <eg-fm-record-editor idlClass="aoa" readonlyFields="org_unit"
+ [mode]="addr(type).isnew() ? 'create': 'update'"
+ [hideBanner]="true" displayMode="inline" hiddenFields="id"
+ (recordSaved)="addrSaved($event)"
+ [record]="addr(type)"
+ fieldOrder="address_type,street1,street2,city,county,state,country,post_code,san,valid"
+ >
+ <eg-fm-record-editor-action i18n-label label="Delete" *ngIf="!addr(type).isnew()"
+ (actionClick)="deleteAddress($event)" buttonCss="btn-warning">
+ </eg-fm-record-editor-action>
+ </eg-fm-record-editor>
+
+ <ng-container *ngIf="sharedAddress(addr(type).id())">
+ <div class="alert alert-info">
+ <span i18n>This address is used for multiple address types.</span>
+ <button (click)="cloneAddress(type)"
+ class="btn btn-light ml-3" i18n>Clone As New Address</button>
+ </div>
+ </ng-container>
+ </ng-template>
+ </ngb-tab>
+ </ng-container>
+</ngb-tabset>
+
--- /dev/null
+import {Component, Input, Output, EventEmitter} from '@angular/core';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+
+const ADDR_TYPES =
+ ['billing_address', 'holds_address', 'mailing_address', 'ill_address'];
+
+@Component({
+ selector: 'eg-admin-org-address',
+ templateUrl: './org-addr.component.html'
+})
+export class OrgAddressComponent {
+
+ orgUnit: IdlObject = null;
+ private tabName: string;
+
+ private _orgId: number;
+
+ get orgId(): number { return this._orgId; }
+
+ @Input() set orgId(newId: number) {
+ if (newId) {
+ if (!this._orgId || this._orgId !== newId) {
+ this._orgId = newId;
+ this.init();
+ }
+ } else {
+ this._orgId = null;
+ }
+ }
+
+ @Output() addrChange: EventEmitter<IdlObject>;
+
+ constructor(
+ private idl: IdlService,
+ private org: OrgService,
+ private pcrud: PcrudService
+ ) {
+ this.addrChange = new EventEmitter<IdlObject>();
+ this.tabName = 'billing_address';
+ }
+
+ init() {
+ if (!this.orgId) { return; }
+
+ return this.pcrud.retrieve('aou', this.orgId,
+ {flesh : 1, flesh_fields : {aou : ADDR_TYPES}},
+ {authoritative: true}
+ ).subscribe(org => {
+ this.orgUnit = org;
+ ADDR_TYPES.forEach(aType => {
+ if (!this.addr(aType)) {
+ this.createAddress(aType);
+ }
+ });
+ });
+ }
+
+ tabChanged($event: NgbTabChangeEvent) {
+ this.tabName = $event.nextId;
+ }
+
+ addrTypes(): string[] { // for UI
+ return ADDR_TYPES;
+ }
+
+ // Template shorthand -- get a specific address by type.
+ addr(addrType: string) {
+ return this.orgUnit ? this.orgUnit[addrType]() : null;
+ }
+
+ createAddress(addrType: string) {
+ const addr = this.idl.create('aoa');
+ addr.isnew(true);
+ addr.valid('t');
+ addr.org_unit(this.orgId);
+ this.orgUnit[addrType](addr);
+ }
+
+ cloneAddress(addrType: string) {
+
+ // Find the address
+ let fromAddr: IdlObject;
+ ADDR_TYPES.forEach(aType => {
+ if (aType !== addrType &&
+ this.addr(aType).id() === this.addr(addrType).id()) {
+ fromAddr = this.addr(aType);
+ }
+ });
+
+ const addr = this.idl.clone(fromAddr);
+ addr.id(null);
+ addr.isnew(true);
+ addr.valid('t');
+ this.orgUnit[addrType](addr);
+ }
+
+ // True if the provided address is used for more than one addr type.
+ sharedAddress(addrId: number): boolean {
+ return ADDR_TYPES.filter(aType => {
+ return (
+ !this.addr(aType).isnew() && this.addr(aType).id() === addrId
+ );
+ }).length > 1;
+ }
+
+ deleteAddress($event: any) {
+ const addr = $event.record;
+ const tmpOrg = this.updatableOrg();
+
+ // Set the FKey to NULL on the org unit for deleted addresses
+ ADDR_TYPES.forEach(aType => {
+ const a = this.addr(aType);
+ if (a && a.id() === addr.id()) {
+ tmpOrg[aType](null);
+ this.createAddress(aType);
+ }
+ });
+
+ this.pcrud.update(tmpOrg).toPromise()
+ .then(_ => this.pcrud.remove(addr).toPromise())
+ .then(_ => this.addrChange.emit(addr));
+ }
+
+ // Addr saved by fm-editor.
+ // In the case of new address creation, point the org unit at
+ // the new address ID.
+ addrSaved(addr: number | IdlObject) {
+
+ if (typeof addr !== 'object') {
+ // pcrud returns a number on 'update' calls. No need to
+ // reload the data on a simple address change. it's changed
+ // in place.
+ return;
+ }
+
+ // update local copy with version that has an ID.
+ this.orgUnit[this.tabName](addr);
+
+ const org = this.updatableOrg();
+ org[this.tabName](addr.id());
+
+ // Creating a new address -- tell our org about it.
+ this.pcrud.update(org).toPromise().then(_ => this.addrChange.emit(addr));
+ }
+
+ // Create an unfleshed org unit object that's a clone of this.orgUnit
+ // to use when pushing updates to the server.
+ updatableOrg(): IdlObject {
+ const org = this.idl.clone(this.orgUnit);
+
+ ADDR_TYPES.forEach(aType => {
+ const addr = this.addr(aType);
+ if (addr) { org[aType](addr.id()); }
+ });
+
+ return org;
+ }
+
+}
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {OrgUnitComponent} from './org-unit.component';
+
+// Since org-unit admin has its own module with page-level components,
+// it needs its own routing module as well to define which component
+// to display at page load time.
+
+const routes: Routes = [{
+ path: '',
+ component: OrgUnitComponent
+}];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+
+export class OrgUnitRoutingModule {}
--- /dev/null
+<eg-staff-banner bannerText="Org Unit Configuration" i18n-bannerText>
+</eg-staff-banner>
+
+<ng-template #editStrTmpl i18n>Update Succeeded</ng-template>
+<eg-string #editString [template]="editStrTmpl"></eg-string>
+
+<ng-template #errorStrTmpl i18n>Update Failed</ng-template>
+<eg-string #errorString [template]="errorStrTmpl"></eg-string>
+
+<eg-confirm-dialog #delConfirm
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Confirm Delete"
+ dialogBody="Delete Org Unit {{selected ? selected.label : ''}}?">
+</eg-confirm-dialog>
+
+<ng-template #treeNodeLabelTmpl let-org="org">
+ <span *ngIf="org" i18n>{{org.name()}} -- {{org.shortname()}}</span>
+</ng-template>
+<eg-string #treeNodeLabel key='admin.server.org_unit.treenode'
+ [template]="treeNodeLabelTmpl"></eg-string>
+
+<div class="row">
+ <div class="col-lg-4">
+ <h3 i18n>Org Units</h3>
+ <eg-tree [tree]="tree" (nodeClicked)="nodeClicked($event)"></eg-tree>
+ </div>
+ <div class="col-lg-8">
+ <div class="alert alert-info">
+ <div *ngIf="currentOrg()">
+ <span *ngIf="currentOrg().name()" i18n>
+ {{currentOrg().name()}} ({{currentOrg().shortname()}})
+ </span>
+ <span *ngIf="!currentOrg().name()" class="font-italic" i18n>
+ Add Name
+ </span>
+ </div>
+ </div>
+ <ngb-tabset #rootTabs (tabChange)="tabChanged($event)" *ngIf="currentOrg()">
+ <ngb-tab title="Main Settings" i18n-title id="main">
+ <ng-template ngbTabContent>
+ <div class="mt-2">
+ <eg-fm-record-editor *ngIf="currentOrg()" #editDialog idlClass="aou"
+ [mode]="currentOrg().isnew() ? 'create': 'update'" [hideBanner]="true"
+ (recordSaved)="orgSaved($event)" displayMode="inline"
+ (recordDeleted)="orgDeleted()"
+ readonlyFields="parent,parent_ou" [preloadLinkedValues]="true"
+ [fieldOptions]="{ou_type: {customValues: orgTypeOptions()}}"
+ [record]="currentOrg().isnew() ? currentOrg() : null"
+ [recordId]="currentOrg().isnew() ? null : currentOrg().id()"
+ [showDelete]="!orgHasChildren()"
+ fieldOrder="parent_ou,ou_type,name,shortname,phone,email,opac_visible,fiscal_calendar"
+ hiddenFields="id,billing_address,mailing_address,holds_address,ill_address">
+ <eg-fm-record-editor-action label="Add Child" i18n-label
+ [disabled]="orgChildTypes().length === 0 || currentOrg().isnew()"
+ buttonCss="btn-outline-info"
+ (actionClick)="addChild()"></eg-fm-record-editor-action>
+ </eg-fm-record-editor>
+ </div>
+ </ng-template>
+ </ngb-tab>
+ <ngb-tab title="Hours of Operation" i18n-title id="hours"
+ [disabled]="currentOrg().isnew()">
+ <ng-template ngbTabContent>
+ <div class="mt-2 common-form striped-even">
+ <div class="row font-weight-bold mb-2">
+ <div class="col-lg-3 offset-lg-2" i18n>Open Time</div>
+ <div class="col-lg-3" i18n>Close Time</div>
+ </div>
+ <div class="row mb-2" *ngFor="let dow of [0,1,2,3,4,5,6]">
+ <div class="col-lg-2" [ngSwitch]="dow">
+ <span *ngSwitchCase="0" i18n>Monday</span>
+ <span *ngSwitchCase="1" i18n>Tuesday</span>
+ <span *ngSwitchCase="2" i18n>Wednesday</span>
+ <span *ngSwitchCase="3" i18n>Thursday</span>
+ <span *ngSwitchCase="4" i18n>Friday</span>
+ <span *ngSwitchCase="5" i18n>Saturday</span>
+ <span *ngSwitchCase="6" i18n>Sunday</span>
+ </div>
+ <div class="col-lg-3">
+ <input class="form-control" type='time' step="60"
+ [ngModel]="hours(dow, 'open')" min="00:00:00" max="23:59:59"
+ (ngModelChange)="hours(dow, 'open', $event)"/>
+ </div>
+ <div class="col-lg-3">
+ <input class="form-control" type='time' step="60"
+ [ngModel]="hours(dow, 'close')" min="00:00:00" max="23:59:59"
+ (ngModelChange)="hours(dow, 'close', $event)"/>
+ </div>
+ <div class="col-lg-2">
+ <button class="btn btn-outline-dark" (click)="closedOn(dow)"
+ [disabled]="isClosed(dow)" i18n>Closed</button>
+ </div>
+ </div>
+ <div class="row d-flex justify-content-end">
+ <div class="alert alert-warning mr-2 p-1"
+ *ngIf="currentOrg().hours_of_operation().isnew()">
+ Hours of Operation Have Not Yet Been Saved.
+ </div>
+ <div class="mr-2">
+ <button class="btn btn-warning" (click)="deleteHours()" i18n>
+ Clear Hours of Operation
+ </button>
+ </div>
+ <div>
+ <button class="btn btn-info" (click)="saveHours()" i18n>
+ Apply Changes
+ </button>
+ </div>
+ <div class="col-lg-2"><!-- alignment --></div>
+ </div>
+ </div>
+ </ng-template>
+ </ngb-tab>
+ <ngb-tab title="Addresses" i18n-title id="addresses"
+ [disabled]="currentOrg().isnew()">
+ <ng-template ngbTabContent>
+ <div class="mt-2">
+ <eg-admin-org-address [orgId]="currentOrg().id()" (addrChange)="addressChanged($event)">
+ </eg-admin-org-address>
+ </div>
+ </ng-template>
+ </ngb-tab>
+ </ngb-tabset>
+ </div>
+</div>
--- /dev/null
+import {Component, Input, ViewChild, OnInit} from '@angular/core';
+import {Tree, TreeNode} from '@eg/share/tree/tree';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {StringService} from '@eg/share/string/string.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+ templateUrl: './org-unit.component.html'
+})
+export class OrgUnitComponent implements OnInit {
+
+ tree: Tree;
+ selected: TreeNode;
+ @ViewChild('editString') editString: StringComponent;
+ @ViewChild('errorString') errorString: StringComponent;
+ @ViewChild('delConfirm') delConfirm: ConfirmDialogComponent;
+
+ constructor(
+ private idl: IdlService,
+ private org: OrgService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private strings: StringService,
+ private toast: ToastService
+ ) {}
+
+
+ ngOnInit() {
+ this.loadAouTree(this.org.root().id());
+ }
+
+ tabChanged(evt: NgbTabChangeEvent) {
+ const tab = evt.nextId;
+ // stubbing out in case we need it.
+ }
+
+ orgSaved(orgId: number | IdlObject) {
+ let id;
+
+ if (orgId) { // new org created, focus it.
+ id = typeof orgId === 'object' ? orgId.id() : orgId;
+ } else if (this.currentOrg()) {
+ id = this.currentOrg().id();
+ }
+
+ this.loadAouTree(id).then(_ => this.postUpdate(this.editString));
+ }
+
+ orgDeleted() {
+ this.loadAouTree();
+ }
+
+ loadAouTree(selectNodeId?: number): Promise<any> {
+
+ const flesh = ['children', 'ou_type', 'hours_of_operation'];
+
+ return this.pcrud.search('aou', {parent_ou : null},
+ {flesh : -1, flesh_fields : {aou : flesh}}, {authoritative: true}
+
+ ).toPromise().then(tree => {
+ this.ingestAouTree(tree);
+ if (!selectNodeId) { selectNodeId = this.org.root().id(); }
+
+ const node = this.tree.findNode(selectNodeId);
+ this.selected = node;
+ this.tree.selectNode(node);
+ });
+ }
+
+ // Translate the org unt type tree into a structure EgTree can use.
+ ingestAouTree(aouTree) {
+
+ const handleNode = (orgNode: IdlObject): TreeNode => {
+ if (!orgNode) { return; }
+
+ if (!orgNode.hours_of_operation()) {
+ this.generateHours(orgNode);
+ }
+
+ const treeNode = new TreeNode({
+ id: orgNode.id(),
+ label: orgNode.name(),
+ callerData: {orgUnit: orgNode}
+ });
+
+ // Apply the compiled label asynchronously
+ this.strings.interpolate(
+ 'admin.server.org_unit.treenode', {org: orgNode}
+ ).then(label => treeNode.label = label);
+
+ orgNode.children().forEach(childNode =>
+ treeNode.children.push(handleNode(childNode))
+ );
+
+ return treeNode;
+ };
+
+ const rootNode = handleNode(aouTree);
+ this.tree = new Tree(rootNode);
+ }
+
+ nodeClicked($event: any) {
+ this.selected = $event;
+ }
+
+ generateHours(org: IdlObject) {
+ const hours = this.idl.create('aouhoo');
+ hours.id(org.id());
+ hours.isnew(true);
+
+ [0, 1, 2, 3, 4, 5, 6].forEach(dow => {
+ this.hours(dow, 'open', '09:00:00', hours);
+ this.hours(dow, 'close', '17:00:00', hours);
+ });
+
+ org.hours_of_operation(hours);
+ }
+
+ // if a 'value' is passed, it will be applied to the optional
+ // hours-of-operation object, otherwise the hours on the currently
+ // selected org unit.
+ hours(dow: number, which: 'open' | 'close', value?: string, hoo?: IdlObject): string {
+ if (!hoo && !this.selected) { return null; }
+
+ const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
+
+ if (value) {
+ hours[`dow_${dow}_${which}`](value);
+ hours.ischanged(true);
+ }
+
+ return hours[`dow_${dow}_${which}`]();
+ }
+
+ isClosed(dow: number): boolean {
+ return (
+ this.hours(dow, 'open') === '00:00:00' &&
+ this.hours(dow, 'close') === '00:00:00'
+ );
+ }
+
+ closedOn(dow: number) {
+ this.hours(dow, 'open', '00:00:00');
+ this.hours(dow, 'close', '00:00:00');
+ }
+
+ saveHours() {
+ const org = this.currentOrg();
+ const hours = org.hours_of_operation();
+ this.pcrud.autoApply(hours).subscribe(
+ result => {
+ console.debug('Hours saved ', result);
+ this.editString.current()
+ .then(msg => this.toast.success(msg));
+ },
+ error => {
+ this.errorString.current()
+ .then(msg => this.toast.danger(msg));
+ },
+ () => this.loadAouTree(this.selected.id)
+ );
+ }
+
+ deleteHours() {
+ const hours = this.currentOrg().hours_of_operation();
+ const promise = hours.isnew() ? Promise.resolve() :
+ this.pcrud.remove(hours).toPromise();
+
+ promise.then(_ => this.generateHours(this.currentOrg()));
+ }
+
+ currentOrg(): IdlObject {
+ return this.selected ? this.selected.callerData.orgUnit : null;
+ }
+
+ orgHasChildren(): boolean {
+ const org = this.currentOrg();
+ return (org && org.children().length > 0);
+ }
+
+ postUpdate(message: StringComponent) {
+ // Modifying org unit types means refetching the org unit
+ // data normally fetched on page load, since it includes
+ // org unit type data.
+ this.org.fetchOrgs().then(() =>
+ message.current().then(str => this.toast.success(str)));
+ }
+
+ remove() {
+ this.delConfirm.open().subscribe(confirmed => {
+ if (!confirmed) { return; }
+
+ const org = this.selected.callerData.orgUnit;
+
+ this.pcrud.remove(org).subscribe(
+ ok2 => {},
+ err => {
+ this.errorString.current()
+ .then(str => this.toast.danger(str));
+ },
+ () => {
+ // Avoid updating until we know the entire
+ // pcrud action/transaction completed.
+ // After removal, select the parent org if available
+ // otherwise the root org.
+ const orgId = org.parent_ou() ?
+ org.parent_ou() : this.org.root().id();
+ this.loadAouTree(orgId).then(_ =>
+ this.postUpdate(this.editString));
+ }
+ );
+ });
+ }
+
+ orgTypeOptions(): ComboboxEntry[] {
+ let ouType = this.currentOrg().ou_type();
+
+ if (typeof ouType === 'number') {
+ // May not be fleshed for new org units
+ ouType = this.org.typeMap()[ouType];
+ }
+ const curDepth = ouType.depth();
+
+ return this.org.typeList()
+ .filter(type_ => type_.depth() === curDepth)
+ .map(type_ => ({id: type_.id(), label: type_.name()}));
+ }
+
+ orgChildTypes(): IdlObject[] {
+ let ouType = this.currentOrg().ou_type();
+
+ if (typeof ouType === 'number') {
+ // May not be fleshed for new org units
+ ouType = this.org.typeMap()[ouType];
+ }
+
+ const depth = ouType.depth();
+ return this.org.typeList()
+ .filter(type_ => type_.depth() === depth + 1);
+ }
+
+ addChild() {
+ const parentTreeNode = this.selected;
+ const parentOrg = this.currentOrg();
+ const newType = this.orgChildTypes()[0];
+
+ const org = this.idl.create('aou');
+ org.isnew(true);
+ org.parent_ou(parentOrg.id());
+ org.ou_type(newType.id());
+ org.children([]);
+
+ // Create a dummy, detached org node to keep the UI happy.
+ this.selected = new TreeNode({
+ id: org.id(),
+ label: org.name(),
+ callerData: {orgUnit: org}
+ });
+ }
+
+ addressChanged(thing: any) {
+ // Reload to pick up org unit address changes.
+ this.orgSaved(this.currentOrg().id());
+ }
+}
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {TreeModule} from '@eg/share/tree/tree.module';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {OrgUnitComponent} from './org-unit.component';
+import {OrgAddressComponent} from './org-addr.component';
+import {OrgUnitRoutingModule} from './org-unit-routing.module';
+
+@NgModule({
+ declarations: [
+ OrgUnitComponent,
+ OrgAddressComponent
+ ],
+ imports: [
+ AdminCommonModule,
+ OrgUnitRoutingModule,
+ TreeModule
+ ],
+ exports: [
+ ],
+ providers: [
+ ]
+})
+
+export class OrgUnitModule {
+}
+
+
path: 'permission/grp_tree',
component: PermGroupTreeComponent
}, {
+ path: 'actor/org_unit',
+ loadChildren: '@eg/staff/admin/server/org-unit.module#OrgUnitModule'
+}, {
path: ':schema/:table',
component: BasicAdminPageComponent
}];
--- /dev/null
+Angular Org Unit Admin Page
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Migrate the Administration => Server Administration => Organizational Units
+page to Angular.
+