finalised edit item page

This commit is contained in:
lotte
2019-02-19 10:13:49 +01:00
parent 86115c44ce
commit bfa1e77177
19 changed files with 346 additions and 213 deletions

View File

@@ -188,7 +188,7 @@
"confirm": "Withdraw", "confirm": "Withdraw",
"cancel": "Cancel", "cancel": "Cancel",
"success": "The item was withdrawn successfully", "success": "The item was withdrawn successfully",
"error": "An error occured while withdrawing the item" "error": "An error occurred while withdrawing the item"
}, },
"reinstate": { "reinstate": {
"header": "Reinstate item: {{ id }}", "header": "Reinstate item: {{ id }}",
@@ -196,7 +196,7 @@
"confirm": "Reinstate", "confirm": "Reinstate",
"cancel": "Cancel", "cancel": "Cancel",
"success": "The item was reinstated successfully", "success": "The item was reinstated successfully",
"error": "An error occured while reinstating the item" "error": "An error occurred while reinstating the item"
}, },
"private": { "private": {
"header": "Make item private: {{ id }}", "header": "Make item private: {{ id }}",
@@ -204,7 +204,7 @@
"confirm": "Make it Private", "confirm": "Make it Private",
"cancel": "Cancel", "cancel": "Cancel",
"success": "The item is now private", "success": "The item is now private",
"error": "An error occured while making the item private" "error": "An error occurred while making the item private"
}, },
"public": { "public": {
"header": "Make item public: {{ id }}", "header": "Make item public: {{ id }}",
@@ -212,7 +212,7 @@
"confirm": "Make it Public", "confirm": "Make it Public",
"cancel": "Cancel", "cancel": "Cancel",
"success": "The item is now public", "success": "The item is now public",
"error": "An error occured while making the item public" "error": "An error occurred while making the item public"
}, },
"delete": { "delete": {
"header": "Delete item: {{ id }}", "header": "Delete item: {{ id }}",
@@ -220,7 +220,7 @@
"confirm": "Delete", "confirm": "Delete",
"cancel": "Cancel", "cancel": "Cancel",
"success": "The item has been deleted", "success": "The item has been deleted",
"error": "An error occured while deleting the item" "error": "An error occurred while deleting the item"
}, },
"metadata": { "metadata": {
"add-button": "Add", "add-button": "Add",
@@ -233,6 +233,14 @@
"language": "Lang", "language": "Lang",
"edit": "Edit" "edit": "Edit"
}, },
"edit": {
"buttons": {
"edit": "Edit",
"unedit": "Stop editing",
"remove": "Remove",
"undo": "Undo changes"
}
},
"metadatafield": { "metadatafield": {
"invalid": "Please choose a valid metadata field" "invalid": "Please choose a valid metadata field"
}, },
@@ -247,7 +255,7 @@
}, },
"invalid": { "invalid": {
"title": "Metadata invalid", "title": "Metadata invalid",
"content": "Please make sure all fields are valid" "content": "Your changes were not saved. Please make sure all fields are valid before you save."
} }
} }
} }

View File

@@ -1,37 +1,21 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<h2 class="border-bottom">{{'item.edit.head' | translate}}</h2> <h2 class="border-bottom">{{'item.edit.head' | translate}}</h2>
<div class="pt-2"> <div class="pt-2">
<ngb-tabset [activeId]="(params$ | async)?.page || 'status'"> <ul class="nav nav-tabs justify-content-start">
<ngb-tab [id]="'status'" title="{{'item.edit.tabs.status.head' | translate}}"> <li *ngFor="let page of pages" class="nav-item">
<ng-template ngbTabContent> <a class="nav-link"
<ds-item-status [item]="(itemRD$ | async)?.payload"></ds-item-status> [ngClass]="{'active' : page === currentPage}"
</ng-template> [routerLink]="'/items/' + (itemRD$ | async)?.payload.uuid + '/edit/' + page">
</ngb-tab> {{'item.edit.tabs.' + page + '.head' | translate}}
<ngb-tab [id]="'bitstreams'" title="{{'item.edit.tabs.bitstreams.head' | translate}}"> </a>
<ng-template ngbTabContent> </li>
</ul>
</ng-template> <div class="tab-pane active">
</ngb-tab> <router-outlet></router-outlet>
<ngb-tab [id]="'metadata'" title="{{'item.edit.tabs.metadata.head' | translate}}"> </div>
<ng-template ngbTabContent> </div>
<ds-item-metadata [item]="(itemRD$ | async)?.payload"> </div>
</ds-item-metadata>
</ng-template>
</ngb-tab>
<ngb-tab [id]="'view'" title="{{'item.edit.tabs.view.head' | translate}}">
<ng-template ngbTabContent>
</ng-template>
</ngb-tab>
<ngb-tab [id]="'curate'" title="{{'item.edit.tabs.curate.head' | translate}}">
<ng-template ngbTabContent>
</ng-template>
</ngb-tab>
</ngb-tabset>
</div>
</div> </div>
</div>
</div> </div>

View File

@@ -1,10 +1,11 @@
import {fadeIn, fadeInOut} from '../../shared/animations/fade'; import { fadeIn, fadeInOut } from '../../shared/animations/fade';
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute, Params, Router } from '@angular/router';
import {RemoteData} from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import {Item} from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import {Observable} from 'rxjs'; import { Observable } from 'rxjs';
import {map} from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { isNotEmpty } from '../../shared/empty.util';
@Component({ @Component({
selector: 'ds-edit-item-page', selector: 'ds-edit-item-page',
@@ -24,13 +25,27 @@ export class EditItemPageComponent implements OnInit {
* The item to edit * The item to edit
*/ */
itemRD$: Observable<RemoteData<Item>>; itemRD$: Observable<RemoteData<Item>>;
params$: Observable<Params>;
constructor(private route: ActivatedRoute) { /**
* The current page outlet string
*/
currentPage: string;
/**
* All possible page outlet strings
*/
pages: string[];
constructor(private route: ActivatedRoute, private router: Router) {
this.router.events.subscribe(() => {
this.currentPage = this.route.snapshot.firstChild.routeConfig.path;
});
} }
ngOnInit(): void { ngOnInit(): void {
this.pages = this.route.routeConfig.children
.map((child: any) => child.path)
.filter((path: string) => isNotEmpty(path)); // ignore reroutes
this.itemRD$ = this.route.data.pipe(map((data) => data.item)); this.itemRD$ = this.route.data.pipe(map((data) => data.item));
this.params$ = this.route.params;
} }
} }

View File

@@ -1,12 +1,14 @@
import {ItemPageResolver} from '../item-page.resolver'; import { ItemPageResolver } from '../item-page.resolver';
import {NgModule} from '@angular/core'; import { NgModule } from '@angular/core';
import {RouterModule} from '@angular/router'; import { RouterModule } from '@angular/router';
import {EditItemPageComponent} from './edit-item-page.component'; import { EditItemPageComponent } from './edit-item-page.component';
import {ItemWithdrawComponent} from './item-withdraw/item-withdraw.component'; import { ItemWithdrawComponent } from './item-withdraw/item-withdraw.component';
import {ItemReinstateComponent} from './item-reinstate/item-reinstate.component'; import { ItemReinstateComponent } from './item-reinstate/item-reinstate.component';
import {ItemPrivateComponent} from './item-private/item-private.component'; import { ItemPrivateComponent } from './item-private/item-private.component';
import {ItemPublicComponent} from './item-public/item-public.component'; import { ItemPublicComponent } from './item-public/item-public.component';
import {ItemDeleteComponent} from './item-delete/item-delete.component'; import { ItemDeleteComponent } from './item-delete/item-delete.component';
import { ItemStatusComponent } from './item-status/item-status.component';
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
const ITEM_EDIT_WITHDRAW_PATH = 'withdraw'; const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
const ITEM_EDIT_REINSTATE_PATH = 'reinstate'; const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
@@ -25,7 +27,36 @@ const ITEM_EDIT_DELETE_PATH = 'delete';
component: EditItemPageComponent, component: EditItemPageComponent,
resolve: { resolve: {
item: ItemPageResolver item: ItemPageResolver
} },
children: [
{
path: '',
redirectTo: 'status'
},
{
path: 'status',
component: ItemStatusComponent
},
{
path: 'bitstreams',
/* TODO - change when bitstreams page exists */
component: ItemStatusComponent
},
{
path: 'metadata',
component: ItemMetadataComponent
},
{
path: 'view',
/* TODO - change when view page exists */
component: ItemStatusComponent
},
{
path: 'curate',
/* TODO - change when curate page exists */
component: ItemStatusComponent
},
]
}, },
{ {
path: ITEM_EDIT_WITHDRAW_PATH, path: ITEM_EDIT_WITHDRAW_PATH,

View File

@@ -7,7 +7,7 @@
<!--{{metadata?.uuid}}--> <!--{{metadata?.uuid}}-->
<td class="col-3"> <td class="col-3">
<div *ngIf="!(editable | async)"> <div *ngIf="!(editable | async)">
<span>{{metadata?.key}}</span> <span>{{metadata?.key?.split('.').join('.&#8203;')}}</span>
</div> </div>
<div *ngIf="(editable | async)" class="field-container"> <div *ngIf="(editable | async)" class="field-container">
<ds-input-suggestions [suggestions]="(metadataFieldSuggestions | async)" <ds-input-suggestions [suggestions]="(metadataFieldSuggestions | async)"
@@ -15,9 +15,12 @@
(submitSuggestion)="update(suggestionControl.control)" (submitSuggestion)="update(suggestionControl.control)"
(clickSuggestion)="update(suggestionControl.control)" (clickSuggestion)="update(suggestionControl.control)"
(typeSuggestion)="update(suggestionControl.control)" (typeSuggestion)="update(suggestionControl.control)"
(blur)="checkValidity(suggestionControl.control)"
(findSuggestions)="findMetadataFieldSuggestions($event)" (findSuggestions)="findMetadataFieldSuggestions($event)"
#suggestionControl="ngModel" #suggestionControl="ngModel"
[dsInListValidator]="metadataFields | async" [dsInListValidator]="metadataFields | async"
[valid]="(valid | async)"
dsAutoFocus autoFocusSelector=".suggestion_input"
></ds-input-suggestions> ></ds-input-suggestions>
</div> </div>
<small class="text-danger" <small class="text-danger"
@@ -43,17 +46,17 @@
</td> </td>
<td class="col-2 text-center"> <td class="col-2 text-center">
<div class="btn-group"> <div class="btn-group">
<button [disabled]="!(canSetEditable() | async)" *ngIf="!(editable | async)" (click)="setEditable(true)" class="btn btn-light btn-sm"> <button [disabled]="!(canSetEditable() | async)" *ngIf="!(editable | async)" (click)="setEditable(true)" class="btn btn-primary btn-sm" title="{{'item.edit.metadata.edit.buttons.edit' | translate}}">
<i class="fas fa-edit fa-fw text-primary"></i> <i class="fas fa-edit fa-fw"></i>
</button> </button>
<button [disabled]="!(canSetUneditable() | async)" *ngIf="(editable | async)" (click)="setEditable(false)" class="btn btn-light btn-sm"> <button [disabled]="!(canSetUneditable() | async)" *ngIf="(editable | async)" (click)="setEditable(false)" class="btn btn-success btn-sm" title="{{'item.edit.metadata.edit.buttons.unedit' | translate}}">
<i class="fas fa-check fa-fw text-success"></i> <i class="fas fa-check fa-fw"></i>
</button> </button>
<button [disabled]="!(canRemove() | async)" (click)="remove()" class="btn btn-light btn-sm"> <button [disabled]="!(canRemove() | async)" (click)="remove()" class="btn btn-danger btn-sm" title="{{'item.edit.metadata.edit.buttons.remove' | translate}}">
<i class="fas fa-trash-alt fa-fw text-danger"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
<button [disabled]="!(canUndo() | async)" (click)="removeChangesFromField()" class="btn btn-light btn-sm"> <button [disabled]="!(canUndo() | async)" (click)="removeChangesFromField()" class="btn btn-warning btn-sm" title="{{'item.edit.metadata.edit.buttons.undo' | translate}}">
<i class="fas fa-undo-alt fa-fw text-warning"></i> <i class="fas fa-undo-alt fa-fw"></i>
</button> </button>
</div> </div>
</td> </td>

View File

@@ -44,7 +44,7 @@ const metadatum = Object.assign(new Metadatum(), {
language: 'en' language: 'en'
}); });
const route = 'http://test-url.com/test-url'; const url = 'http://test-url.com/test-url';
const fieldUpdate = { const fieldUpdate = {
field: metadatum, field: metadatum,
changeType: undefined changeType: undefined
@@ -92,7 +92,7 @@ describe('EditInPlaceFieldComponent', () => {
de = fixture.debugElement.query(By.css('div.d-flex')); de = fixture.debugElement.query(By.css('div.d-flex'));
el = de.nativeElement; el = de.nativeElement;
comp.route = route; comp.url = url;
comp.fieldUpdate = fieldUpdate; comp.fieldUpdate = fieldUpdate;
comp.metadata = metadatum; comp.metadata = metadatum;
@@ -104,8 +104,8 @@ describe('EditInPlaceFieldComponent', () => {
comp.update(); comp.update();
}); });
it('it should call saveChangeFieldUpdate on the objectUpdatesService with the correct route and metadata', () => { it('it should call saveChangeFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
expect(objectUpdatesService.saveChangeFieldUpdate).toHaveBeenCalledWith(route, metadatum); expect(objectUpdatesService.saveChangeFieldUpdate).toHaveBeenCalledWith(url, metadatum);
}); });
}); });
@@ -145,8 +145,8 @@ describe('EditInPlaceFieldComponent', () => {
comp.setEditable(editable); comp.setEditable(editable);
}); });
it('it should call setEditableFieldUpdate on the objectUpdatesService with the correct route and uuid and false', () => { it('it should call setEditableFieldUpdate on the objectUpdatesService with the correct url and uuid and false', () => {
expect(objectUpdatesService.setEditableFieldUpdate).toHaveBeenCalledWith(route, metadatum.uuid, editable); expect(objectUpdatesService.setEditableFieldUpdate).toHaveBeenCalledWith(url, metadatum.uuid, editable);
}); });
}); });
@@ -203,8 +203,8 @@ describe('EditInPlaceFieldComponent', () => {
comp.remove(); comp.remove();
}); });
it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct route and metadata', () => { it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(route, metadatum); expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(url, metadatum);
}); });
}); });
@@ -213,8 +213,8 @@ describe('EditInPlaceFieldComponent', () => {
comp.removeChangesFromField(); comp.removeChangesFromField();
}); });
it('it should call removeChangesFromField on the objectUpdatesService with the correct route and uuid', () => { it('it should call removeChangesFromField on the objectUpdatesService with the correct url and uuid', () => {
expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(route, metadatum.uuid); expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, metadatum.uuid);
}); });
}); });

View File

@@ -10,7 +10,6 @@ import { InputSuggestion } from '../../../../shared/input-suggestions/input-sugg
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer'; import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { inListValidator } from '../../../../shared/utils/validator.functions';
import { getSucceededRemoteData } from '../../../../core/shared/operators'; import { getSucceededRemoteData } from '../../../../core/shared/operators';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
@@ -28,9 +27,9 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
*/ */
@Input() fieldUpdate: FieldUpdate; @Input() fieldUpdate: FieldUpdate;
/** /**
* The current route of this page * The current url of this page
*/ */
@Input() route: string; @Input() url: string;
/** /**
* The metadatum of this field * The metadatum of this field
*/ */
@@ -65,8 +64,8 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
* Sets up an observable that keeps track of the current editable and valid state of this field * Sets up an observable that keeps track of the current editable and valid state of this field
*/ */
ngOnInit(): void { ngOnInit(): void {
this.editable = this.objectUpdatesService.isEditable(this.route, this.metadata.uuid); this.editable = this.objectUpdatesService.isEditable(this.url, this.metadata.uuid);
this.valid = this.objectUpdatesService.isValid(this.route, this.metadata.uuid); this.valid = this.objectUpdatesService.isValid(this.url, this.metadata.uuid);
this.metadataFields = this.findMetadataFields() this.metadataFields = this.findMetadataFields()
} }
@@ -74,32 +73,41 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
* Sends a new change update for this field to the object updates service * Sends a new change update for this field to the object updates service
*/ */
update(control?: FormControl) { update(control?: FormControl) {
this.objectUpdatesService.saveChangeFieldUpdate(this.route, this.metadata); this.objectUpdatesService.saveChangeFieldUpdate(this.url, this.metadata);
if (hasValue(control)) { if (hasValue(control)) {
this.objectUpdatesService.setValidFieldUpdate(this.route, this.metadata.uuid, control.valid); this.checkValidity(control);
} }
} }
/**
* Method to check the validity of a form control
* @param control The form control to check
*/
private checkValidity(control: FormControl) {
control.updateValueAndValidity();
this.objectUpdatesService.setValidFieldUpdate(this.url, this.metadata.uuid, control.valid);
}
/** /**
* Sends a new editable state for this field to the service to change it * Sends a new editable state for this field to the service to change it
* @param editable The new editable state for this field * @param editable The new editable state for this field
*/ */
setEditable(editable: boolean) { setEditable(editable: boolean) {
this.objectUpdatesService.setEditableFieldUpdate(this.route, this.metadata.uuid, editable); this.objectUpdatesService.setEditableFieldUpdate(this.url, this.metadata.uuid, editable);
} }
/** /**
* Sends a new remove update for this field to the object updates service * Sends a new remove update for this field to the object updates service
*/ */
remove() { remove() {
this.objectUpdatesService.saveRemoveFieldUpdate(this.route, this.metadata); this.objectUpdatesService.saveRemoveFieldUpdate(this.url, this.metadata);
} }
/** /**
* Notifies the object updates service that the updates for the current field can be removed * Notifies the object updates service that the updates for the current field can be removed
*/ */
removeChangesFromField() { removeChangesFromField() {
this.objectUpdatesService.removeSingleFieldUpdate(this.route, this.metadata.uuid); this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.metadata.uuid);
} }
/** /**
@@ -170,15 +178,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
* @return an observable that emits true when the user should be able to remove this field and false when they should not * @return an observable that emits true when the user should be able to remove this field and false when they should not
*/ */
canRemove(): Observable<boolean> { canRemove(): Observable<boolean> {
return this.editable.pipe( return observableOf(this.fieldUpdate.changeType !== FieldChangeType.REMOVE && this.fieldUpdate.changeType !== FieldChangeType.ADD);
map((editable: boolean) => {
if (editable) {
return false;
} else {
return this.fieldUpdate.changeType !== FieldChangeType.REMOVE && this.fieldUpdate.changeType !== FieldChangeType.ADD;
}
})
);
} }
/** /**
@@ -186,7 +186,9 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
* @return an observable that emits true when the user should be able to undo changes to this field and false when they should not * @return an observable that emits true when the user should be able to undo changes to this field and false when they should not
*/ */
canUndo(): Observable<boolean> { canUndo(): Observable<boolean> {
return observableOf(this.fieldUpdate.changeType >= 0); return this.editable.pipe(
map((editable: boolean) => this.fieldUpdate.changeType >= 0 || editable)
);
} }
protected isNotEmpty(value): boolean { protected isNotEmpty(value): boolean {

View File

@@ -5,8 +5,7 @@
(click)="add()"><i (click)="add()"><i
class="fas fa-plus"></i> {{"item.edit.metadata.add-button" | translate}} class="fas fa-plus"></i> {{"item.edit.metadata.add-button" | translate}}
</button> </button>
<div class="my-2 spaced-btn-group">
<div class="btn-group btn-group-toggle my-2" data-toggle="buttons">
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)" <button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)" [disabled]="!(hasChanges() | async)"
(click)="discard()"><i (click)="discard()"><i
@@ -32,24 +31,26 @@
</tr> </tr>
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"> <tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate">
<ds-edit-in-place-field [fieldUpdate]="updateValue || {}" <ds-edit-in-place-field [fieldUpdate]="updateValue || {}"
[route]="route"></ds-edit-in-place-field> [url]="url"></ds-edit-in-place-field>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="btn-group btn-group-toggle my-2 float-right" data-toggle="buttons"> <div class="button-row">
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)" <div class="my-2 float-right spaced-btn-group">
[disabled]="!(hasChanges() | async)" <button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
(click)="discard()"><i [disabled]="!(hasChanges() | async)"
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}} (click)="discard()"><i
</button> class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
<button class="btn btn-warning" *ngIf="isReinstatable() | async" </button>
(click)="reinstate()"><i <button class="btn btn-warning" *ngIf="isReinstatable() | async"
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}} (click)="reinstate()"><i
</button> class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)" </button>
(click)="submit()"><i <button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}} (click)="submit()"><i
</button> class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,13 @@
@import '../../../../styles/variables.scss'; @import '../../../../styles/variables.scss';
.button-row .btn { .button-row {
min-width: $button-min-width; .spaced-btn-group > .btn {
margin-right: 0.5 * $spacer;
&:last-child {
margin-right: 0;
}
}
.btn {
min-width: $button-min-width;
}
} }

View File

@@ -7,7 +7,7 @@ import { Metadatum } from '../../../core/shared/metadatum.model';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { SharedModule } from '../../../shared/shared.module'; import { SharedModule } from '../../../shared/shared.module';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { ItemDataService } from '../../../core/data/item-data.service'; import { ItemDataService } from '../../../core/data/item-data.service';
@@ -32,6 +32,8 @@ const infoNotification: INotification = new Notification('id', NotificationType.
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
const date = new Date(); const date = new Date();
const router = new RouterStub(); const router = new RouterStub();
let routeStub;
let itemService; let itemService;
const notificationsService = jasmine.createSpyObj('notificationsService', const notificationsService = jasmine.createSpyObj('notificationsService',
{ {
@@ -56,9 +58,9 @@ const metadatum3 = Object.assign(new Metadatum(), {
value: 'Shakespeare, William', value: 'Shakespeare, William',
}); });
const route = 'http://test-url.com/test-url'; const url = 'http://test-url.com/test-url';
router.url = route; router.url = url;
const fieldUpdate1 = { const fieldUpdate1 = {
field: metadatum1, field: metadatum1,
@@ -84,6 +86,11 @@ describe('ItemMetadataComponent', () => {
update: observableOf(new RemoteData(false, false, true, undefined, item)), update: observableOf(new RemoteData(false, false, true, undefined, item)),
commitUpdates: {} commitUpdates: {}
}); });
routeStub = {
parent: {
data: observableOf({ item: new RemoteData(false, false, true, null, item) })
}
};
scheduler = getTestScheduler(); scheduler = getTestScheduler();
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
{ {
@@ -111,6 +118,9 @@ describe('ItemMetadataComponent', () => {
{ provide: ItemDataService, useValue: itemService }, { provide: ItemDataService, useValue: itemService },
{ provide: ObjectUpdatesService, useValue: objectUpdatesService }, { provide: ObjectUpdatesService, useValue: objectUpdatesService },
{ provide: Router, useValue: router }, { provide: Router, useValue: router },
{
provide: ActivatedRoute, useValue: routeStub
},
{ provide: NotificationsService, useValue: notificationsService }, { provide: NotificationsService, useValue: notificationsService },
{ provide: GLOBAL_CONFIG, useValue: { notifications: { timeOut: 10 } } as any } { provide: GLOBAL_CONFIG, useValue: { notifications: { timeOut: 10 } } as any }
], schemas: [ ], schemas: [
@@ -118,16 +128,14 @@ describe('ItemMetadataComponent', () => {
] ]
}).compileComponents(); }).compileComponents();
}) })
) );
;
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ItemMetadataComponent); fixture = TestBed.createComponent(ItemMetadataComponent);
comp = fixture.componentInstance; // EditInPlaceFieldComponent test instance comp = fixture.componentInstance; // EditInPlaceFieldComponent test instance
de = fixture.debugElement.query(By.css('div.d-flex')); de = fixture.debugElement.query(By.css('div.d-flex'));
el = de.nativeElement; el = de.nativeElement;
comp.item = item; comp.url = url;
comp.route = route;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -137,8 +145,8 @@ describe('ItemMetadataComponent', () => {
comp.add(md); comp.add(md);
}); });
it('it should call saveAddFieldUpdate on the objectUpdatesService with the correct route and metadata', () => { it('it should call saveAddFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
expect(objectUpdatesService.saveAddFieldUpdate).toHaveBeenCalledWith(route, md); expect(objectUpdatesService.saveAddFieldUpdate).toHaveBeenCalledWith(url, md);
}); });
}); });
@@ -147,8 +155,8 @@ describe('ItemMetadataComponent', () => {
comp.discard(); comp.discard();
}); });
it('it should call discardFieldUpdates on the objectUpdatesService with the correct route and notification', () => { it('it should call discardFieldUpdates on the objectUpdatesService with the correct url and notification', () => {
expect(objectUpdatesService.discardFieldUpdates).toHaveBeenCalledWith(route, infoNotification); expect(objectUpdatesService.discardFieldUpdates).toHaveBeenCalledWith(url, infoNotification);
}); });
}); });
@@ -157,8 +165,8 @@ describe('ItemMetadataComponent', () => {
comp.reinstate(); comp.reinstate();
}); });
it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct route', () => { it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct url', () => {
expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(route); expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(url);
}); });
}); });
@@ -167,10 +175,10 @@ describe('ItemMetadataComponent', () => {
comp.submit(); comp.submit();
}); });
it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct route and metadata', () => { it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct url and metadata', () => {
expect(objectUpdatesService.getUpdatedFields).toHaveBeenCalledWith(route, comp.item.metadata); expect(objectUpdatesService.getUpdatedFields).toHaveBeenCalledWith(url, comp.item.metadata);
expect(itemService.update).toHaveBeenCalledWith(comp.item); expect(itemService.update).toHaveBeenCalledWith(comp.item);
expect(objectUpdatesService.getFieldUpdates).toHaveBeenCalledWith(route, comp.item.metadata); expect(objectUpdatesService.getFieldUpdates).toHaveBeenCalledWith(url, comp.item.metadata);
}); });
}); });

View File

@@ -2,7 +2,7 @@ import { Component, Inject, Input, OnInit } from '@angular/core';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { ItemDataService } from '../../../core/data/item-data.service'; import { ItemDataService } from '../../../core/data/item-data.service';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { import {
@@ -31,15 +31,15 @@ export class ItemMetadataComponent implements OnInit {
/** /**
* The item to display the edit page for * The item to display the edit page for
*/ */
@Input() item: Item; item: Item;
/** /**
* The current values and updates for all this item's metadata fields * The current values and updates for all this item's metadata fields
*/ */
updates$: Observable<FieldUpdates>; updates$: Observable<FieldUpdates>;
/** /**
* The current route of this page * The current url of this page
*/ */
route: string; url: string;
/** /**
* The time span for being able to undo discarding changes * The time span for being able to undo discarding changes
*/ */
@@ -51,16 +51,25 @@ export class ItemMetadataComponent implements OnInit {
private router: Router, private router: Router,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private translateService: TranslateService, private translateService: TranslateService,
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
private route: ActivatedRoute
) { ) {
} }
ngOnInit(): void { ngOnInit(): void {
this.route.parent.data.pipe(map((data) => data.item))
.pipe(
first(),
map((data: RemoteData<Item>) => data.payload)
).subscribe((item: Item) => {
this.item = item;
});
this.discardTimeOut = this.EnvConfig.notifications.timeOut; this.discardTimeOut = this.EnvConfig.notifications.timeOut;
this.route = this.router.url; this.url = this.router.url;
if (this.route.indexOf('?') > 0) { if (this.url.indexOf('?') > 0) {
this.route = this.route.substr(0, this.route.indexOf('?')); this.url = this.url.substr(0, this.url.indexOf('?'));
} }
this.hasChanges().pipe(first()).subscribe((hasChanges) => { this.hasChanges().pipe(first()).subscribe((hasChanges) => {
if (!hasChanges) { if (!hasChanges) {
@@ -69,7 +78,8 @@ export class ItemMetadataComponent implements OnInit {
this.checkLastModified(); this.checkLastModified();
} }
}); });
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.route, this.item.metadata); this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadata);
} }
/** /**
@@ -77,7 +87,8 @@ export class ItemMetadataComponent implements OnInit {
* @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum * @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum
*/ */
add(metadata: Metadatum = new Metadatum()) { add(metadata: Metadatum = new Metadatum()) {
this.objectUpdatesService.saveAddFieldUpdate(this.route, metadata); this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata);
} }
/** /**
@@ -88,21 +99,21 @@ export class ItemMetadataComponent implements OnInit {
const title = this.translateService.instant('item.edit.metadata.notifications.discarded.title'); const title = this.translateService.instant('item.edit.metadata.notifications.discarded.title');
const content = this.translateService.instant('item.edit.metadata.notifications.discarded.content'); const content = this.translateService.instant('item.edit.metadata.notifications.discarded.content');
const undoNotification = this.notificationsService.info(title, content, { timeOut: this.discardTimeOut }); const undoNotification = this.notificationsService.info(title, content, { timeOut: this.discardTimeOut });
this.objectUpdatesService.discardFieldUpdates(this.route, undoNotification); this.objectUpdatesService.discardFieldUpdates(this.url, undoNotification);
} }
/** /**
* Request the object updates service to undo discarding all changes to this item * Request the object updates service to undo discarding all changes to this item
*/ */
reinstate() { reinstate() {
this.objectUpdatesService.reinstateFieldUpdates(this.route); this.objectUpdatesService.reinstateFieldUpdates(this.url);
} }
/** /**
* Sends all initial values of this item to the object updates service * Sends all initial values of this item to the object updates service
*/ */
private initializeOriginalFields() { private initializeOriginalFields() {
this.objectUpdatesService.initialize(this.route, this.item.metadata, this.item.lastModified); this.objectUpdatesService.initialize(this.url, this.item.metadata, this.item.lastModified);
} }
/* Prevent unnecessary rerendering so fields don't lose focus **/ /* Prevent unnecessary rerendering so fields don't lose focus **/
@@ -117,42 +128,42 @@ export class ItemMetadataComponent implements OnInit {
submit() { submit() {
this.isValid().pipe(first()).subscribe((isValid) => { this.isValid().pipe(first()).subscribe((isValid) => {
if (isValid) { if (isValid) {
const metadata$: Observable<Identifiable[]> = this.objectUpdatesService.getUpdatedFields(this.route, this.item.metadata) as Observable<Metadatum[]>; const metadata$: Observable<Identifiable[]> = this.objectUpdatesService.getUpdatedFields(this.url, this.item.metadata) as Observable<Metadatum[]>;
metadata$.pipe( metadata$.pipe(
first(), first(),
switchMap((metadata: Metadatum[]) => { switchMap((metadata: Metadatum[]) => {
const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata }); const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata });
return this.itemService.update(updatedItem); return this.itemService.update(updatedItem);
}), }),
tap(() => this.itemService.commitUpdates()), tap(() => this.itemService.commitUpdates()),
getSucceededRemoteData() getSucceededRemoteData()
).subscribe( ).subscribe(
(rd: RemoteData<Item>) => { (rd: RemoteData<Item>) => {
this.item = rd.payload; this.item = rd.payload;
this.initializeOriginalFields(); this.initializeOriginalFields();
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.route, this.item.metadata); this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadata);
} }
) )
} else { } else {
const title = this.translateService.instant('item.edit.metadata.notifications.invalid.title'); const title = this.translateService.instant('item.edit.metadata.notifications.invalid.title');
const content = this.translateService.instant('item.edit.metadata.notifications.invalid.content'); const content = this.translateService.instant('item.edit.metadata.notifications.invalid.content');
this.notificationsService.error(title, content); this.notificationsService.error(title, content);
} }
}); });
} }
/** /**
* Checks whether or not there are currently updates for this item * Checks whether or not there are currently updates for this item
*/ */
hasChanges(): Observable<boolean> { hasChanges(): Observable<boolean> {
return this.objectUpdatesService.hasUpdates(this.route); return this.objectUpdatesService.hasUpdates(this.url);
} }
/** /**
* Checks whether or not the item is currently reinstatable * Checks whether or not the item is currently reinstatable
*/ */
isReinstatable(): Observable<boolean> { isReinstatable(): Observable<boolean> {
return this.objectUpdatesService.isReinstatable(this.route); return this.objectUpdatesService.isReinstatable(this.url);
} }
/** /**
@@ -161,7 +172,7 @@ export class ItemMetadataComponent implements OnInit {
*/ */
private checkLastModified() { private checkLastModified() {
const currentVersion = this.item.lastModified; const currentVersion = this.item.lastModified;
this.objectUpdatesService.getLastModified(this.route).pipe(first()).subscribe( this.objectUpdatesService.getLastModified(this.url).pipe(first()).subscribe(
(updateVersion: Date) => { (updateVersion: Date) => {
if (updateVersion.getDate() !== currentVersion.getDate()) { if (updateVersion.getDate() !== currentVersion.getDate()) {
const title = this.translateService.instant('item.edit.metadata.notifications.outdated.title'); const title = this.translateService.instant('item.edit.metadata.notifications.outdated.title');
@@ -173,7 +184,10 @@ export class ItemMetadataComponent implements OnInit {
); );
} }
/**
* Check if the current page is entirely valid
*/
private isValid() { private isValid() {
return this.objectUpdatesService.isValidPage(this.route); return this.objectUpdatesService.isValidPage(this.url);
} }
} }

View File

@@ -6,11 +6,13 @@ import { CommonModule } from '@angular/common';
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub'; import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
import { HostWindowService } from '../../../shared/host-window.service'; import { HostWindowService } from '../../../shared/host-window.service';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { RouterStub } from '../../../shared/testing/router-stub'; import { RouterStub } from '../../../shared/testing/router-stub';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
describe('ItemStatusComponent', () => { describe('ItemStatusComponent', () => {
let comp: ItemStatusComponent; let comp: ItemStatusComponent;
@@ -27,12 +29,19 @@ describe('ItemStatusComponent', () => {
url: `${itemPageUrl}/edit` url: `${itemPageUrl}/edit`
}); });
const routeStub = {
parent: {
data: observableOf({ item: new RemoteData(false, false, true, null, mockItem) })
}
};
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
declarations: [ItemStatusComponent], declarations: [ItemStatusComponent],
providers: [ providers: [
{ provide: Router, useValue: routerStub }, { provide: Router, useValue: routerStub },
{ provide: ActivatedRoute, useValue: routeStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) } { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
], schemas: [CUSTOM_ELEMENTS_SCHEMA] ], schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -41,7 +50,6 @@ describe('ItemStatusComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ItemStatusComponent); fixture = TestBed.createComponent(ItemStatusComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.item = mockItem;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -65,4 +73,5 @@ describe('ItemStatusComponent', () => {
expect(statusItemPage.textContent).toContain(itemPageUrl); expect(statusItemPage.textContent).toContain(itemPageUrl);
}); });
}); })
;

View File

@@ -1,8 +1,11 @@
import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import {fadeIn, fadeInOut} from '../../../shared/animations/fade'; import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
import {Item} from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import {Router} from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import {ItemOperation} from '../item-operation/itemOperation.model'; import { ItemOperation } from '../item-operation/itemOperation.model';
import { first, map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
@Component({ @Component({
selector: 'ds-item-status', selector: 'ds-item-status',
@@ -21,7 +24,7 @@ export class ItemStatusComponent implements OnInit {
/** /**
* The item to display the status for * The item to display the status for
*/ */
@Input() item: Item; itemRD$: Observable<RemoteData<Item>>;
/** /**
* The data to show in the status * The data to show in the status
@@ -37,39 +40,46 @@ export class ItemStatusComponent implements OnInit {
* key: id value: url to action's component * key: id value: url to action's component
*/ */
operations: ItemOperation[]; operations: ItemOperation[];
/** /**
* The keys of the actions (to loop over) * The keys of the actions (to loop over)
*/ */
actionsKeys; actionsKeys;
constructor(private router: Router) { constructor(private router: Router, private route: ActivatedRoute) {
} }
ngOnInit(): void { ngOnInit(): void {
this.statusData = Object.assign({ this.itemRD$ = this.route.parent.data.pipe(map((data) => data.item));
id: this.item.id, this.itemRD$.pipe(
handle: this.item.handle, first(),
lastModified: this.item.lastModified map((data: RemoteData<Item>) => data.payload)
).subscribe((item: Item) => {
this.statusData = Object.assign({
id: item.id,
handle: item.handle,
lastModified: item.lastModified
});
this.statusDataKeys = Object.keys(this.statusData);
/*
The key is used to build messages
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
The value is supposed to be a href for the button
*/
this.operations = [];
if (item.isWithdrawn) {
this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl() + '/reinstate'));
} else {
this.operations.push(new ItemOperation('withdraw', this.getCurrentUrl() + '/withdraw'));
}
if (item.isDiscoverable) {
this.operations.push(new ItemOperation('private', this.getCurrentUrl() + '/private'));
} else {
this.operations.push(new ItemOperation('public', this.getCurrentUrl() + '/public'));
}
this.operations.push(new ItemOperation('delete', this.getCurrentUrl() + '/delete'));
}); });
this.statusDataKeys = Object.keys(this.statusData);
/*
The key is used to build messages
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
The value is supposed to be a href for the button
*/
this.operations = [];
if (this.item.isWithdrawn) {
this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl() + '/reinstate'));
} else {
this.operations.push(new ItemOperation('withdraw', this.getCurrentUrl() + '/withdraw'));
}
if (this.item.isDiscoverable) {
this.operations.push(new ItemOperation('private', this.getCurrentUrl() + '/private'));
} else {
this.operations.push(new ItemOperation('public', this.getCurrentUrl() + '/public'));
}
this.operations.push(new ItemOperation('delete', this.getCurrentUrl() + '/delete'));
} }
/** /**

View File

@@ -15,7 +15,7 @@ export function getItemEditPath(id: string) {
return new URLCombiner(getItemModulePath(),ITEM_EDIT_PATH.replace(/:id/, id)).toString() return new URLCombiner(getItemModulePath(),ITEM_EDIT_PATH.replace(/:id/, id)).toString()
} }
const ITEM_EDIT_PATH = ':id/edit/:page'; const ITEM_EDIT_PATH = ':id/edit';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -39,7 +39,7 @@ const ITEM_EDIT_PATH = ':id/edit/:page';
path: ITEM_EDIT_PATH, path: ITEM_EDIT_PATH,
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule', loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
canActivate: [AuthenticatedGuard] canActivate: [AuthenticatedGuard]
} },
]) ])
], ],
providers: [ providers: [

View File

@@ -22,10 +22,10 @@ export class MetadataField implements ListableObject {
@autoserialize @autoserialize
schema: MetadataSchema; schema: MetadataSchema;
toString(): string { toString(separator: string = '.'): string {
let key = this.schema.prefix + '.' + this.element; let key = this.schema.prefix + separator + this.element;
if (isNotEmpty(this.qualifier)) { if (isNotEmpty(this.qualifier)) {
key += '.' + this.qualifier; key += separator + this.qualifier;
} }
return key; return key;
} }

View File

@@ -5,8 +5,10 @@
(dsClickOutside)="close()"> (dsClickOutside)="close()">
<input #inputField type="text" [(ngModel)]="value" [name]="name" <input #inputField type="text" [(ngModel)]="value" [name]="name"
class="form-control suggestion_input" class="form-control suggestion_input"
[ngClass]="{'is-invalid': !valid}"
[dsDebounce]="debounceTime" (onDebounce)="find($event)" [dsDebounce]="debounceTime" (onDebounce)="find($event)"
[placeholder]="placeholder" [placeholder]="placeholder"
(blur)="blur.emit($event);"
[ngModelOptions]="{standalone: true}" autocomplete="off"/> [ngModelOptions]="{standalone: true}" autocomplete="off"/>
<input type="submit" class="d-none"/> <input type="submit" class="d-none"/>
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}"> <div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">

View File

@@ -60,6 +60,11 @@ export class InputSuggestionsComponent implements ControlValueAccessor, OnChange
*/ */
@Input() name; @Input() name;
/**
* Whether or not the current input is valid
*/
@Input() valid;
/** /**
* Output for when the form is submitted * Output for when the form is submitted
*/ */
@@ -80,6 +85,11 @@ export class InputSuggestionsComponent implements ControlValueAccessor, OnChange
*/ */
@Output() findSuggestions = new EventEmitter(); @Output() findSuggestions = new EventEmitter();
/**
* Emits event when the input field loses focus
*/
@Output() blur = new EventEmitter();
/** /**
* Emits true when the list of suggestions should be shown * Emits true when the list of suggestions should be shown
*/ */
@@ -238,4 +248,8 @@ export class InputSuggestionsComponent implements ControlValueAccessor, OnChange
this._value = val; this._value = val;
this.propagateChange(this._value); this.propagateChange(this._value);
} }
focus() {
this.queryInput.nativeElement.focus();
}
} }

View File

@@ -93,6 +93,7 @@ import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/del
import { LangSwitchComponent } from './lang-switch/lang-switch.component'; import { LangSwitchComponent } from './lang-switch/lang-switch.component';
import { ObjectValuesPipe } from './utils/object-values-pipe'; import { ObjectValuesPipe } from './utils/object-values-pipe';
import { InListValidator } from './utils/in-list-validator.directive'; import { InListValidator } from './utils/in-list-validator.directive';
import { AutoFocusDirective } from './utils/auto-focus.directive';
const MODULES = [ const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here // Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -199,7 +200,8 @@ const DIRECTIVES = [
DragClickDirective, DragClickDirective,
DebounceDirective, DebounceDirective,
ClickOutsideDirective, ClickOutsideDirective,
InListValidator InListValidator,
AutoFocusDirective
]; ];
@NgModule({ @NgModule({

View File

@@ -0,0 +1,22 @@
import { Directive, AfterViewInit, ElementRef, Input } from '@angular/core';
import { isNotEmpty } from '../empty.util';
@Directive({
selector: '[dsAutoFocus]'
})
export class AutoFocusDirective implements AfterViewInit {
@Input() autoFocusSelector: string;
constructor(private el: ElementRef) {
}
ngAfterViewInit() {
if (isNotEmpty(this.autoFocusSelector)) {
return this.el.nativeElement.querySelector(this.autoFocusSelector).focus();
} else {
return this.el.nativeElement.focus();
}
}
}