this.loginSessionKeys.push(key);
}
- setItem(key: string, val: any, isJson?: Boolean): Promise<any> {
+ setItem(key: string, val: any, isJson?: Boolean): Promise<void> {
// TODO: route keys appropriately
this.setLocalItem(key, val, false);
return Promise.resolve();
window.localStorage.setItem(key, val);
}
- setServerItem(key: string, val: any): Promise<any> {
+ setServerItem(key: string, val: any): Promise<void> {
return Promise.resolve();
}
window.localStorage.removeItem(key);
}
- removeServerItem(key: string): Promise<any> {
+ removeServerItem(key: string): Promise<void> {
return Promise.resolve();
}
</eg-confirm-dialog>
<div class="row">
- <div class="col-8 offset-1">
- <div class="alert alert-warning" *ngIf="removingWs" i18n>
- Workstation {{removingWs}} is no longer valid. Removing registration.
+ <div class="col-8 offset-1 mt-3">
+ <div class="alert alert-warning" *ngIf="removeWorkstation" i18n>
+ Workstation {{removeWorkstation}} is no longer valid. Removing registration.
</div>
<div class="alert alert-danger" *ngIf="workstations.length == 0">
<span i18n>Please register a workstation.</span>
<div class="row mt-2">
<div class="col-2">
<eg-org-select
- (onChange)="orgOnChange"
+ (onChange)="orgOnChange($event)"
[hideOrgs]="hideOrgs"
[disableOrgs]="disableOrgs"
[initialOrg]="initialOrg"
placeholder="Workstation Name"
[(ngModel)]='newName'/>
<div class="input-group-btn">
- <button class="btn btn-light" (click)="registerWorkstation()">
+ <button class="btn btn-outline-dark"
+ [disabled]="!newName || !newOwner"
+ (click)="registerWorkstation()">
<span i18n>Register</span>
</button>
</div>
</div>
</div>
<div class="row">
- <div class="col-6">
- <select
- class="form-control"
- [(ngModel)]="selectedId">
- <option *ngFor="let ws of workstations" value="{{ws.id}}">
- {{ws.name}}
+ <div class="col-8">
+ <select class="form-control" [(ngModel)]="selectedName">
+ <option *ngFor="let ws of workstations" value="{{ws.name}}">
+ <span *ngIf="ws.name == defaultName" i18n>
+ {{ws.name}} (Default)
+ </span>
+ <span *ngIf="ws.name != defaultName">
+ {{ws.name}}
+ </span>
</option>
</select>
</div>
(click)="useNow()" [disabled]="!selected">
Use Now
</button>
- <button i18n class="btn btn-light"
+ <button i18n class="btn btn-outline-dark"
(click)="setDefault()" [disabled]="!selected">
Mark As Default
</button>
import {Component, OnInit, ViewChild} from '@angular/core';
-import {ActivatedRoute} from '@angular/router';
+import {Router, ActivatedRoute} from '@angular/router';
import {EgStoreService} from '@eg/core/store';
import {EgIdlObject} from '@eg/core/idl';
import {EgNetService} from '@eg/core/net';
import {EgAuthService} from '@eg/core/auth';
import {EgOrgService} from '@eg/core/org';
+import {EgEventService} from '@eg/core/event';
import {EgConfirmDialogComponent} from '@eg/share/confirm-dialog.component';
// Slim version of the WS that's stored in the cache.
})
export class WorkstationsComponent implements OnInit {
- selectedId: Number;
+ selectedName: string;
workstations: Workstation[] = [];
removeWorkstation: string;
newOwner: EgIdlObject;
- newName: string = 'NewTestName';
+ newName: string;
+ defaultName: string;
@ViewChild('workstationExistsDialog')
private wsExistsDialog: EgConfirmDialogComponent;
}
constructor(
+ private router: Router,
private route: ActivatedRoute,
+ private evt: EgEventService,
private net: EgNetService,
private store: EgStoreService,
private auth: EgAuthService,
ngOnInit() {
this.store.getItem('eg.workstation.all')
- .then(res => this.workstations = res);
+ .then(list => this.workstations = list || [])
+ .then(noop => this.store.getItem('eg.workstation.default'))
+ .then(defWs => {
+ this.defaultName = defWs;
+ this.selectedName = this.auth.workstation() || defWs
+ })
+ .then(noop => {
+ let rm = this.route.snapshot.paramMap.get('remove');
+ if (rm) this.removeSelected(this.removeWorkstation = rm)
+ })
// TODO: perm limits required here too
this.disableOrgs = this.org.filterList({canHaveUsers : true}, true);
-
- this.removeWorkstation = this.route.snapshot.paramMap.get('remove');
- if (this.removeWorkstation) {
- console.log('Removing workstation ' + this.removeWorkstation);
- // TODO remove
- }
}
selected(): Workstation {
return this.workstations.filter(
- ws => {return ws.id == this.selectedId})[0];
+ ws => {return ws.name == this.selectedName})[0];
}
useNow(): void {
- //console.debug('using ' + this.selected().name);
-
- this.wsExistsDialog.open().then(
- confirmed => console.log('dialog confirmed'),
- dismissed => console.log('dialog dismissed')
- );
+ if (!this.selected()) return;
+ this.router.navigate(['/staff/login'],
+ {queryParams: {workstation: this.selected().name}});
}
setDefault(): void {
- console.debug('defaulting ' + this.selected().name);
+ if (!this.selected()) return;
+ this.defaultName = this.selected().name;
+ this.store.setItem('eg.workstation.default', this.defaultName);
}
- removeSelected(): void {
- console.debug('removing ' + this.selected().name);
+ removeSelected(name?: string): void {
+ if (!name) name = this.selected().name;
+
+ this.workstations = this.workstations.filter(w => w.name != name);
+ this.store.setItem('eg.workstation.all', this.workstations);
+
+ if (this.defaultName == name) {
+ this.defaultName = null;
+ this.store.removeItem('eg.workstation.default');
+ }
}
canDeleteSelected(): boolean {
registerWorkstation(): void {
console.log(`Registering new workstation ` +
`"${this.newName}" at ${this.newOwner.shortname()}`);
+
+ this.newName = this.newOwner.shortname() + '-' + this.newName;
+
+ this.registerWorkstationApi().then(
+ wsId => this.registerWorkstationLocal(wsId));
+
+ }
+
+ private handleCollision(): Promise<number> {
+ return new Promise((resolve, reject) => {
+ this.wsExistsDialog.open()
+ .then(
+ confirmed => {
+ this.registerWorkstationApi(true).then(
+ wsId => resolve(wsId),
+ notOk => reject()
+ )
+ },
+ dismissed => reject()
+ )
+ });
+ }
+
+
+ private registerWorkstationApi(override?: boolean): Promise<number> {
+ let method = 'open-ils.actor.workstation.register';
+ if (override) method += '.override';
+
+ return new Promise((resolve, reject) => {
+ this.net.request(
+ 'open-ils.actor', method,
+ this.auth.token(), this.newName, this.newOwner.id()
+ ).subscribe(wsId => {
+ let evt = this.evt.parse(wsId);
+ if (evt) {
+ if (evt.textcode == 'WORKSTATION_NAME_EXISTS') {
+ this.handleCollision().then(
+ id => resolve(id),
+ notOk => reject(notOk)
+ )
+ } else {
+ console.error(`Registration failed ${evt}`);
+ reject();
+ }
+ } else {
+ resolve(wsId);
+ }
+ });
+ });
+ }
+
+ private registerWorkstationLocal(wsId: number) {
+ let ws: Workstation = {
+ id: wsId,
+ name: this.newName,
+ owning_lib: this.newOwner.id()
+ };
+
+ this.workstations.push(ws);
+ this.store.setItem('eg.workstation.all', this.workstations)
+ .then(ok => this.newName = '');
}
}
export class EgStaffComponent implements OnInit {
readonly loginPath = '/staff/login';
- readonly wsAdminPath = '/staff/admin/workstation/workstations/manage';
+ readonly wsAdminBasePath = '/staff/admin/workstation/workstations/';
constructor(
private router: Router,
}
/**
- * Verifying auth token on every route is overkill, since an expired
- * token will make itself known with the first API call, but we do
+ * Verifying the auth token on every route is overkill, since
+ * an expired token will make itself known with the first API
+ * call or when the auth session poll discovers it, but we do
* want to prevent navigation from the login or workstation admin
* page, since these can be accessed without a valid authtoken or
- * workstation, respectively, once the initial route resolvers
- * have done their jobs.
+ * workstation, respectively, once the initial route resolvers have
+ * done their jobs.
*/
basicAuthChecks(routeEvent: NavigationEnd): void {
// Access to workstation admin page is granted regardless
// of workstation validity.
- if (routeEvent.url == this.wsAdminPath) return;
+ if (routeEvent.url.indexOf(this.wsAdminBasePath) >= 0) return;
if (this.auth.workstationState != EgAuthWsState.VALID)
- this.router.navigate([this.wsAdminPath]);
+ this.router.navigate([this.wsAdminBasePath]);
}
}
EgConfirmDialogComponent
],
imports: [
+ CommonModule,
EgStaffRoutingModule,
FormsModule,
NgbModule
margin-bottom: .1rem;
}
+/* BS default height is 2.25rem + 2px which is quite chunky.
+ * This better matches the text input heights */
+select.form-control:not([size]):not([multiple]) {
+ padding: .355rem .55rem;
+ height: 2.2rem;
+}
+
#staffcat-search-form {
border-bottom: 2px dashed rgba(0,0,0,.225);
}
<div id='staffcat-search-form' class='pb-2 mb-3'>
<div class="row"
*ngFor="let q of searchContext.query; let idx = index; trackBy:trackByIdx">
- <div class="col-9 d-flex flex-row">
+ <div class="col-9 d-flex">
<div class="flex-1">
<div *ngIf="idx == 0">
<select class="form-control" [(ngModel)]="searchContext.format">
</div>
<div class="flex-2 pl-1">
<div class="form-group">
- <input type="text" class="form-control"
- TODOfocus-me="searchContext.focus_query[idx]"
- [(ngModel)]="searchContext.query[idx]"
- (keyup)="checkEnter($event)"
- placeholder="Query..."/>
+ <div *ngIf="idx == 0">
+ <input type="text" class="form-control"
+ id='first-query-input'
+ [(ngModel)]="searchContext.query[idx]"
+ (keyup)="checkEnter($event)"
+ placeholder="Query..."/>
+ </div>
+ <div *ngIf="idx > 0">
+ <input type="text" class="form-control"
+ [(ngModel)]="searchContext.query[idx]"
+ (keyup)="checkEnter($event)"
+ placeholder="Query..."/>
+ </div>
</div>
</div>
<div class="flex-1 pl-1">
</div><!-- row -->
<div class="row">
- <div class="col-9 d-flex flex-row">
+ <div class="col-9 d-flex">
<div class="flex-1">
<eg-org-select
(onChange)="orgOnChange($event)"
-import {Component, OnInit} from '@angular/core';
+import {Component, OnInit, AfterViewInit, Renderer} from '@angular/core';
import {EgIdlObject} from '@eg/core/idl';
import {EgOrgService} from '@eg/core/org';
import {EgCatalogService,} from '@eg/share/catalog/catalog.service';
styleUrls: ['search-form.component.css'],
templateUrl: 'search-form.component.html'
})
-export class SearchFormComponent implements OnInit {
+export class SearchFormComponent implements OnInit, AfterViewInit {
searchContext: CatalogSearchContext;
ccvmMap: {[ccvm:string] : EgIdlObject[]} = {};
showAdvancedSearch: boolean = false;
constructor(
+ private renderer: Renderer,
private org: EgOrgService,
private cat: EgCatalogService,
private staffCat: StaffCatalogService
// Start with advanced search options open
// if any filters are active.
this.showAdvancedSearch = this.hasAdvancedOptions();
+
+ }
+
+ ngAfterViewInit() {
+ // Query inputs are generated from search context data,
+ // so they are not available until after the first render.
+ // Search context data is extracted synchronously from the URL.
+ this.renderer.selectRootElement('#first-query-input').focus();
}
/**
-<div class="col-md-4 offset-md-4">
- <fieldset>
- <legend i18n>Sign In</legend>
- <hr/>
- <form (ngSubmit)="handleSubmit()" #loginForm="ngForm">
+<div class="container">
+ <div class="col-md-6 offset-md-3">
+ <fieldset>
+ <legend class="mb-0" i18n>Sign In</legend>
+ <hr class="mt-1"/>
+ <form (ngSubmit)="handleSubmit()" #loginForm="ngForm">
- <div class="form-group">
- <label for="username" i18n>Username</label>
- <input
- type="text"
- class="form-control"
- id="username"
- name="username"
- required
- i18n-placeholder
- placeholder="Username"
- [(ngModel)]="args.username"/>
- </div>
+ <div class="form-group row">
+ <label class="col-md-4 text-right font-weight-bold" for="username" i18n>Username</label>
+ <input
+ type="text"
+ class="form-control col-md-8"
+ id="username"
+ name="username"
+ required
+ autocomplete="username"
+ i18n-placeholder
+ placeholder="Username"
+ [(ngModel)]="args.username"/>
+ </div>
- <div class="form-group">
- <label for="password" i18n>Password</label>
- <input
- type="password"
- class="form-control"
- id="password"
- name="password"
- required
- i18n-placeholder
- placeholder="Password"
- [(ngModel)]="args.password"/>
- </div>
+ <div class="form-group row">
+ <label class="col-md-4 text-right font-weight-bold" for="password" i18n>Password</label>
+ <input
+ type="password"
+ class="form-control col-md-8"
+ id="password"
+ name="password"
+ required
+ autocomplete="current-password"
+ i18n-placeholder
+ placeholder="Password"
+ [(ngModel)]="args.password"/>
+ </div>
- <button type="submit" class="btn btn-light" i18n>Sign in</button>
- </form>
- </fieldset>
+ <div class="form-group row" *ngIf="workstations && workstations.length">
+ <label class="col-md-4 text-right font-weight-bold" for="workstation" i18n>Workstation</label>
+ <select
+ class="form-control col-md-8"
+ id="workstation"
+ name="workstation"
+ required
+ [(ngModel)]="args.workstation">
+ <option *ngFor="let ws of workstations" value="{{ws.name}}">
+ {{ws.name}}
+ </option>
+ </select>
+ </div>
+
+ <div class="row">
+ <div class="col-md-8 offset-md-4 pl-0">
+ <button type="submit" class="btn btn-outline-dark" i18n>Sign in</button>
+ </div>
+ </div>
+ </form>
+ </fieldset>
+ </div>
</div>
-import { Component, OnInit, Renderer } from '@angular/core';
-import { Location } from '@angular/common';
-import { Router } from '@angular/router';
-import { EgAuthService, EgAuthWsState } from '@eg/core/auth';
-import { EgStoreService } from '@eg/core/store'; // TODO: testing
+import {Component, OnInit, Renderer} from '@angular/core';
+import {Location} from '@angular/common';
+import {Router, ActivatedRoute} from '@angular/router';
+import {EgAuthService, EgAuthWsState} from '@eg/core/auth';
+import {EgStoreService} from '@eg/core/store';
@Component({
templateUrl : './login.component.html'
export class EgStaffLoginComponent implements OnInit {
+ workstations: any[];
+
args = {
username : '',
password : '',
- type : 'staff',
- //workstation : ''
- workstation : 'BR1-skiddoo' // testing
+ workstation : '',
+ type : 'staff'
};
- workstations = [];
-
constructor(
private router: Router,
+ private route: ActivatedRoute,
private ngLocation: Location,
private renderer: Renderer,
private auth: EgAuthService,
// Focus username
this.renderer.selectRootElement('#username').focus();
- // load browser-local workstation data
+ this.store.getItem('eg.workstation.all')
+ .then(list => this.workstations = list || [])
+ .then(list => this.store.getItem('eg.workstation.default'))
+ .then(defWs => this.args.workstation = defWs)
+ .then(noOp => this.applyWorkstation())
+ }
+
+ applyWorkstation() {
+ let wanted = this.route.snapshot.queryParamMap.get('workstation');
+ if (!wanted) return; // use the default
- // TODO: insert for testing.
- this.store.setItem(
- 'eg.workstation.all',
- [{name:'BR1-skiddoo',id:1,owning_lib:4}]
- );
+ let exists = this.workstations.filter(w => w.name == wanted)[0];
+ if (exists) {
+ this.args.workstation = wanted;
+ } else {
+ console.error(`Unknown workstation requested: ${wanted}`);
+ }
}
handleSubmit() {
// User attempted to login with a workstation that is
// unknown to the server. Redirect to the WS admin page.
this.router.navigate(
- ['/staff/admin/workstation/workstations/remove/${workstation}']);
+ [`/staff/admin/workstation/workstations/remove/${workstation}`]);
} else {
// Force reload of the app after a successful login.
// This allows the route resolver to re-run with a
</a>
<div class="dropdown-menu" ngbDropdownMenu>
<a class="dropdown-item"
- routerLink="/staff/catalog/search">
- <span class="material-icons">search</span>
- <span i18n>TODO</span>
+ routerLink="/staff/admin/workstation/workstations/manage">
+ <span class="material-icons">computer</span>
+ <span i18n>Registered Workstations</span>
</a>
</div>
</div>
i18n-title
title="Log out and more..."
class="nav-link dropdown-toggle no-caret with-material-icon">
- <i class="material-icons">list</i>
+ <i class="material-icons">more_vert</i>
</a>
<div class="dropdown-menu" ngbDropdownMenu>
<a i18n class="dropdown-item" routerLink="/staff/login">
<span class="material-icons">lock_outline</span>
<span i18n>Logout</span>
</a>
+ <a i18n class="dropdown-item" href="/eg/staff/about">
+ <span class="material-icons">info_outline</span>
+ <span i18n>About</span>
+ </a>
</div>
</div>
</div>
import {Location} from '@angular/common';
import {Observable, Observer} from 'rxjs/Rx';
import {Router, Resolve, RouterStateSnapshot,
+ ActivatedRoute,
ActivatedRouteSnapshot} from '@angular/router';
import {EgStoreService} from '@eg/core/store';
import {EgNetService} from '@eg/core/net';
export class EgStaffResolver implements Resolve<Observable<any>> {
readonly loginPath = '/staff/login';
- readonly wsAdminPath = '/staff/admin/workstation/workstations/manage';
+ readonly wsRemPath = '/staff/admin/workstation/workstations/remove/';
constructor(
private router: Router,
+ private route: ActivatedRoute,
private ngLocation: Location,
private store: EgStoreService,
private net: EgNetService,
this.store.loginSessionBasePath = '/';
//this.ngLocation.prepareExternalUrl('/staff');
- // Login resets everything. No need to load data.
- if (state.url == '/staff/login') return Observable.of(true);
+ // Not sure how to get the path without params... using this for now.
+ let path = state.url.split('?')[0]
+ if (path == '/staff/login') return Observable.of(true);
return Observable.create(observer => {
this.auth.testAuthToken().then(
);
},
wsNotOk => {
- if (state.url != this.wsAdminPath) {
- this.router.navigate([this.wsAdminPath]);
+ if (path.indexOf(this.wsRemPath) < 0) {
+ this.router.navigate([
+ this.wsRemPath + this.auth.workstation()
+ ]);
}
observer.complete();
}
}
-/** BS has flex utility classes, but none for specifying flex widths */
+/** BS has flex utility classes, but none for specifying flex widths.
+ * BS class="col" is roughly equivelent to flex-1, but col-2 is not
+ * equivalent to flex-2, since col-2 really means 2/12 width. */
.flex-1 {flex: 1}
.flex-2 {flex: 2}
.flex-3 {flex: 3}