mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
intermediate commit for tests
This commit is contained in:
@@ -79,5 +79,8 @@ module.exports = {
|
||||
code: 'nl',
|
||||
label: 'Nederlands',
|
||||
active: false,
|
||||
}]
|
||||
}],
|
||||
item: {
|
||||
|
||||
}
|
||||
};
|
||||
|
@@ -223,7 +223,26 @@
|
||||
"error": "An error occured while deleting the item"
|
||||
},
|
||||
"metadata": {
|
||||
"add-button": "Add new metadata"
|
||||
"add-button": "Add",
|
||||
"discard-button": "Discard",
|
||||
"reinstate-button": "Undo",
|
||||
"save-button": "Save",
|
||||
"headers": {
|
||||
"field": "Field",
|
||||
"value": "Value",
|
||||
"language": "Lang",
|
||||
"edit": "Edit"
|
||||
},
|
||||
"notifications": {
|
||||
"outdated": {
|
||||
"title": "Changed outdated",
|
||||
"content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts"
|
||||
},
|
||||
"discarded": {
|
||||
"title": "Changed discarded",
|
||||
"content": "Your changes were discarded. To reinstate your changes click the 'Undo' button"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -14,9 +14,10 @@
|
||||
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
<ngb-tab title="{{'item.edit.tabs.metadata.head' | translate}}">
|
||||
<ngb-tab [id]="'metadata'" title="{{'item.edit.tabs.metadata.head' | translate}}">
|
||||
<ng-template ngbTabContent>
|
||||
<ds-item-metadata [item]="(itemRD$ | async)?.payload"></ds-item-metadata>
|
||||
<ds-item-metadata [item]="(itemRD$ | async)?.payload">
|
||||
</ds-item-metadata>
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
<ngb-tab title="{{'item.edit.tabs.view.head' | translate}}">
|
||||
|
@@ -13,7 +13,7 @@ import {ItemPrivateComponent} from './item-private/item-private.component';
|
||||
import {ItemPublicComponent} from './item-public/item-public.component';
|
||||
import {ItemDeleteComponent} from './item-delete/item-delete.component';
|
||||
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
||||
import { EditInPlaceComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component';
|
||||
import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component';
|
||||
|
||||
/**
|
||||
* Module that contains all components related to the Edit Item page administrator functionality
|
||||
@@ -36,7 +36,7 @@ import { EditInPlaceComponent } from './item-metadata/edit-in-place-field/edit-i
|
||||
ItemDeleteComponent,
|
||||
ItemStatusComponent,
|
||||
ItemMetadataComponent,
|
||||
EditInPlaceComponent
|
||||
EditInPlaceFieldComponent
|
||||
]
|
||||
})
|
||||
export class EditItemPageModule {
|
||||
|
@@ -1,42 +1,47 @@
|
||||
<td class="col-3">
|
||||
<!--<div *ngIf="!editable">-->
|
||||
<span>{{metadata.key}}</span>
|
||||
<!--</div>-->
|
||||
<!--<div *ngIf="editable" class="field-container">-->
|
||||
<!--<ds-input-suggestions [suggestions]="(filterSearchResults | async)"-->
|
||||
<!--[action]="getCurrentUrl()"-->
|
||||
<!--[name]="filterConfig.paramName"-->
|
||||
<!--[(ngModel)]="metadata.key"-->
|
||||
<!--(submitSuggestion)="updateField($event)"-->
|
||||
<!--(clickSuggestion)="updateField($event)"-->
|
||||
<!--(findSuggestions)="findSuggestions($event)"-->
|
||||
<!--ngDefaultControl-->
|
||||
<!--></ds-input-suggestions>-->
|
||||
<!--</div>-->
|
||||
</td>
|
||||
<td class="col-7">
|
||||
<div *ngIf="!editable">
|
||||
<span>{{metadata.value}}</span>
|
||||
</div>
|
||||
<div *ngIf="editable" class="field-container">
|
||||
<textarea type="textarea" [ngModel]="metadata.value" [dsDebounce] (onDebounce)="update()"></textarea>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-1">
|
||||
<div *ngIf="!editable">
|
||||
<span>{{metadata.language}}</span>
|
||||
</div>
|
||||
<div *ngIf="editable" class="field-container">
|
||||
<input type="text" [ngModel]="metadata.language" [dsDebounce] (onDebounce)="update()"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-1">
|
||||
<div *ngIf="!editable">
|
||||
<i class="fas fa-edit fa-fw" (click)="editable = !editable"></i>
|
||||
<i class="fas fa-trash fa-fw" (click)="remove()"></i>
|
||||
</div>
|
||||
<div *ngIf="editable">
|
||||
<i class="fas fa-times fa-fw" (click)="editable = !editable"></i>
|
||||
<i class="fas fa-trash fa-fw" (click)="remove()"></i>
|
||||
</div>
|
||||
</td>
|
||||
<div [ngClass]="{
|
||||
'table-warning': fieldUpdate.changeType === 0,
|
||||
'table-danger': fieldUpdate.changeType === 2,
|
||||
'table-success': fieldUpdate.changeType === 1
|
||||
}" class="d-flex">
|
||||
<!--{{metadata?.uuid}}-->
|
||||
<td class="col-3">
|
||||
<div *ngIf="!(editable | async)">
|
||||
<span>{{metadata?.key}}</span>
|
||||
</div>
|
||||
<div *ngIf="(editable | async)" class="field-container">
|
||||
<ds-input-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
||||
[(ngModel)]="metadata.key"
|
||||
(submitSuggestion)="update()"
|
||||
(clickSuggestion)="update()"
|
||||
(findSuggestions)="findMetadataFieldSuggestions($event)"
|
||||
ngDefaultControl
|
||||
></ds-input-suggestions>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-7">
|
||||
<div *ngIf="!(editable | async)">
|
||||
<span>{{metadata?.value}}</span>
|
||||
</div>
|
||||
<div *ngIf="(editable | async)" class="field-container">
|
||||
<textarea class="form-control" type="textarea" [(ngModel)]="metadata.value" [dsDebounce]
|
||||
(onDebounce)="update()"></textarea>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-1 text-center">
|
||||
<div *ngIf="!(editable | async)">
|
||||
<span>{{metadata?.language}}</span>
|
||||
</div>
|
||||
<div *ngIf="(editable | async)" class="field-container">
|
||||
<input class="form-control" type="text" [(ngModel)]="metadata.language" [dsDebounce]
|
||||
(onDebounce)="update()"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-1 text-center">
|
||||
<div>
|
||||
<i *ngIf="canSetEditable() | async" class="fas fa-edit fa-fw text-primary" (click)="setEditable(true)"></i>
|
||||
<i *ngIf="canSetUneditable() | async" class="fas fa-check fa-fw text-success" (click)="setEditable(false)"></i>
|
||||
<i *ngIf="canRemove() | async" class="fas fa-trash-alt fa-fw text-danger" (click)="remove()"></i>
|
||||
<i *ngIf="canUndo() | async" class="fas fa-undo-alt fa-fw text-warning" (click)="removeChangesFromField()"></i>
|
||||
</div>
|
||||
</td>
|
||||
</div>
|
@@ -1,3 +0,0 @@
|
||||
textarea, input, select {
|
||||
width: 100%;
|
||||
}
|
@@ -0,0 +1,290 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
||||
import { EditInPlaceFieldComponent } from './edit-in-place-field.component';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
import { MetadataField } from '../../../../core/metadata/metadatafield.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Metadatum } from '../../../../core/shared/metadatum.model';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { SharedModule } from '../../../../shared/shared.module';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadataschema.model';
|
||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||
|
||||
let comp: EditInPlaceFieldComponent;
|
||||
let fixture: ComponentFixture<EditInPlaceFieldComponent>;
|
||||
let de: DebugElement;
|
||||
let el: HTMLElement;
|
||||
let metadataFieldService;
|
||||
let objectUpdatesService;
|
||||
let paginatedMetadataFields;
|
||||
const mdSchema = Object.assign(new MetadataSchema(), { prefix: 'dc' })
|
||||
const mdField1 = Object.assign(new MetadataField(), {
|
||||
schema: mdSchema,
|
||||
element: 'contributor',
|
||||
qualifier: 'author'
|
||||
});
|
||||
const mdField2 = Object.assign(new MetadataField(), { schema: mdSchema, element: 'title' });
|
||||
const mdField3 = Object.assign(new MetadataField(), {
|
||||
schema: mdSchema,
|
||||
element: 'description',
|
||||
qualifier: 'abstract'
|
||||
});
|
||||
|
||||
const metadatum = Object.assign(new Metadatum(), {
|
||||
key: 'dc.description.abstract',
|
||||
value: 'Example abstract',
|
||||
language: 'en'
|
||||
});
|
||||
|
||||
const route = 'http://test-url.com/test-url';
|
||||
const fieldUpdate = {
|
||||
field: metadatum,
|
||||
changeType: undefined
|
||||
};
|
||||
let scheduler: TestScheduler;
|
||||
|
||||
describe('EditInPlaceFieldComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
paginatedMetadataFields = new PaginatedList(undefined, [mdField1, mdField2, mdField3]);
|
||||
|
||||
metadataFieldService = jasmine.createSpyObj({
|
||||
queryMetadataFields: observableOf(new RemoteData(false, false, true, undefined, paginatedMetadataFields))
|
||||
});
|
||||
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
||||
{
|
||||
saveChangeFieldUpdate: {},
|
||||
saveRemoveFieldUpdate: {},
|
||||
setEditableFieldUpdate: {},
|
||||
removeSingleFieldUpdate: {},
|
||||
isEditable: observableOf(false) // should always return something --> its in ngOnInit
|
||||
}
|
||||
);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [FormsModule, SharedModule],
|
||||
declarations: [EditInPlaceFieldComponent],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: metadataFieldService },
|
||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||
], schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditInPlaceFieldComponent);
|
||||
comp = fixture.componentInstance; // EditInPlaceFieldComponent test instance
|
||||
de = fixture.debugElement.query(By.css('div.d-flex'));
|
||||
el = de.nativeElement;
|
||||
|
||||
comp.route = route;
|
||||
comp.fieldUpdate = fieldUpdate;
|
||||
comp.metadata = metadatum;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
beforeEach(() => {
|
||||
comp.update();
|
||||
});
|
||||
|
||||
it('it should call saveChangeFieldUpdate on the objectUpdatesService with the correct route and metadata', () => {
|
||||
expect(objectUpdatesService.saveChangeFieldUpdate).toHaveBeenCalledWith(route, metadatum);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setEditable', () => {
|
||||
const editable = false;
|
||||
beforeEach(() => {
|
||||
comp.setEditable(editable);
|
||||
});
|
||||
|
||||
it('it should call setEditableFieldUpdate on the objectUpdatesService with the correct route and uuid and false', () => {
|
||||
expect(objectUpdatesService.setEditableFieldUpdate).toHaveBeenCalledWith(route, metadatum.uuid, editable);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
beforeEach(() => {
|
||||
comp.remove();
|
||||
});
|
||||
|
||||
it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct route and metadata', () => {
|
||||
expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(route, metadatum);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
beforeEach(() => {
|
||||
comp.remove();
|
||||
});
|
||||
|
||||
it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct route and metadata', () => {
|
||||
expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(route, metadatum);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findMetadataFieldSuggestions', () => {
|
||||
const query = 'query string';
|
||||
|
||||
const metadataFieldSuggestions: InputSuggestion[] =
|
||||
[
|
||||
{ displayValue: mdField1.toString(), value: mdField1.toString() },
|
||||
{ displayValue: mdField2.toString(), value: mdField2.toString() },
|
||||
{ displayValue: mdField3.toString(), value: mdField3.toString() }
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
comp.findMetadataFieldSuggestions(query);
|
||||
|
||||
});
|
||||
|
||||
it('it should call queryMetadataFields on the metadataFieldService with the correct query', () => {
|
||||
|
||||
expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query);
|
||||
});
|
||||
|
||||
it('it should set metadataFieldSuggestions to the right value', () => {
|
||||
const expected = 'a';
|
||||
scheduler.expectObservable(comp.metadataFieldSuggestions).toBe(expected, { a: metadataFieldSuggestions });
|
||||
});
|
||||
});
|
||||
|
||||
describe('canSetEditable', () => {
|
||||
describe('when editable is currently true', () => {
|
||||
beforeEach(() => {
|
||||
comp.editable = observableOf(true);
|
||||
});
|
||||
|
||||
it('canSetEditable should return an observable emitting false', () => {
|
||||
const expected = '(a|)';
|
||||
scheduler.expectObservable(comp.canSetEditable()).toBe(expected, { a: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when editable is currently false', () => {
|
||||
beforeEach(() => {
|
||||
comp.editable = observableOf(false);
|
||||
});
|
||||
|
||||
describe('when the fieldUpdate\'s changeType is currently not REMOVE', () => {
|
||||
beforeEach(() => {
|
||||
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
||||
});
|
||||
it('canSetEditable should return an observable emitting true', () => {
|
||||
const expected = '(a|)';
|
||||
scheduler.expectObservable(comp.canSetEditable()).toBe(expected, { a: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the fieldUpdate\'s changeType is currently REMOVE', () => {
|
||||
beforeEach(() => {
|
||||
comp.fieldUpdate.changeType = FieldChangeType.REMOVE;
|
||||
});
|
||||
it('canSetEditable should return an observable emitting false', () => {
|
||||
const expected = '(a|)';
|
||||
scheduler.expectObservable(comp.canSetEditable()).toBe(expected, { a: false });
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe('canSetUneditable', () => {
|
||||
describe('when editable is currently true', () => {
|
||||
beforeEach(() => {
|
||||
comp.editable = observableOf(true);
|
||||
});
|
||||
|
||||
it('canSetUneditable should return an observable emitting true', () => {
|
||||
const expected = '(a|)';
|
||||
scheduler.expectObservable(comp.canSetUneditable()).toBe(expected, { a: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when editable is currently false', () => {
|
||||
beforeEach(() => {
|
||||
comp.editable = observableOf(false);
|
||||
});
|
||||
|
||||
it('canSetUneditable should return an observable emitting false', () => {
|
||||
const expected = '(a|)';
|
||||
scheduler.expectObservable(comp.canSetUneditable()).toBe(expected, { a: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('canRemove', () => {
|
||||
describe('when editable is currently true', () => {
|
||||
beforeEach(() => {
|
||||
comp.editable = observableOf(true);
|
||||
});
|
||||
it('canRemove should return an observable emitting false', () => {
|
||||
const expected = '(a|)';
|
||||
scheduler.expectObservable(comp.canRemove()).toBe(expected, { a: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when editable is currently false', () => {
|
||||
beforeEach(() => {
|
||||
comp.editable = observableOf(false);
|
||||
});
|
||||
|
||||
describe('when the fieldUpdate\'s changeType is currently not REMOVE or ADD', () => {
|
||||
beforeEach(() => {
|
||||
comp.fieldUpdate.changeType = FieldChangeType.UPDATE;
|
||||
});
|
||||
it('canRemove should return an observable emitting true', () => {
|
||||
const expected = '(a|)';
|
||||
scheduler.expectObservable(comp.canRemove()).toBe(expected, { a: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the fieldUpdate\'s changeType is currently ADD', () => {
|
||||
beforeEach(() => {
|
||||
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
||||
});
|
||||
it('canRemove should return an observable emitting false', () => {
|
||||
const expected = '(a|)';
|
||||
scheduler.expectObservable(comp.canRemove()).toBe(expected, { a: false });
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe('canUndo', () => {
|
||||
|
||||
describe('when the fieldUpdate\'s changeType is currently ADD, UPDATE or REMOVE', () => {
|
||||
beforeEach(() => {
|
||||
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
||||
});
|
||||
|
||||
it('canUndo should return an observable emitting true', () => {
|
||||
const expected = '(a|)';
|
||||
scheduler.expectObservable(comp.canUndo()).toBe(expected, { a: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the fieldUpdate\'s changeType is currently undefined', () => {
|
||||
beforeEach(() => {
|
||||
comp.fieldUpdate.changeType = undefined;
|
||||
});
|
||||
|
||||
it('canUndo should return an observable emitting false', () => {
|
||||
const expected = '(a|)';
|
||||
scheduler.expectObservable(comp.canUndo()).toBe(expected, { a: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,41 +1,169 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Component, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||
import { Metadatum } from '../../../../core/shared/metadatum.model';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
|
||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { MetadataField } from '../../../../core/metadata/metadatafield.model';
|
||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-in-place-field.',
|
||||
selector: 'ds-edit-in-place-field',
|
||||
styleUrls: ['./edit-in-place-field.component.scss'],
|
||||
templateUrl: './edit-in-place-field.component.html',
|
||||
})
|
||||
/**
|
||||
* Component for displaying an item's status
|
||||
* Component that displays a single metadatum of an item on the edit page
|
||||
*/
|
||||
export class EditInPlaceComponent {
|
||||
export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
||||
|
||||
/**
|
||||
* The value to display
|
||||
* The current field, value and state of the metadatum
|
||||
*/
|
||||
@Input() metadata: Metadatum;
|
||||
@Output() mdUpdate: EventEmitter<any> = new EventEmitter();
|
||||
@Output() mdRemove: EventEmitter<any> = new EventEmitter();
|
||||
editable = false;
|
||||
@Input() fieldUpdate: FieldUpdate;
|
||||
/**
|
||||
* The current route of this page
|
||||
*/
|
||||
@Input() route: string;
|
||||
/**
|
||||
* The metadatum of this field
|
||||
*/
|
||||
metadata: Metadatum;
|
||||
/**
|
||||
* Emits whether or not this field is currently editable
|
||||
*/
|
||||
editable: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The current suggestions for the metadatafield when editing
|
||||
*/
|
||||
metadataFieldSuggestions: BehaviorSubject<InputSuggestion[]> = new BehaviorSubject([]);
|
||||
|
||||
constructor(
|
||||
private metadataFieldService: RegistryService,
|
||||
private objectUpdatesService: ObjectUpdatesService,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
isNotEmpty(value) {
|
||||
/**
|
||||
* Sends a new change update for this field to the object updates service
|
||||
*/
|
||||
update() {
|
||||
this.objectUpdatesService.saveChangeFieldUpdate(this.route, this.metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a new editable state for this field to the service to change it
|
||||
* @param editable The new editable state for this field
|
||||
*/
|
||||
setEditable(editable: boolean) {
|
||||
this.objectUpdatesService.setEditableFieldUpdate(this.route, this.metadata.uuid, editable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a new remove update for this field to the object updates service
|
||||
*/
|
||||
remove() {
|
||||
this.objectUpdatesService.saveRemoveFieldUpdate(this.route, this.metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the object updates service that the updates for the current field can be removed
|
||||
*/
|
||||
removeChangesFromField() {
|
||||
this.objectUpdatesService.removeSingleFieldUpdate(this.route, this.metadata.uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up an observable that keeps track of the current editable state of this field
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.editable = this.objectUpdatesService.isEditable(this.route, this.metadata.uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current metadatafield based on the fieldUpdate input field
|
||||
*/
|
||||
ngOnChanges(): void {
|
||||
this.metadata = cloneDeep(this.fieldUpdate.field) as Metadatum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests all metadata fields that contain the query string in their key
|
||||
* Then sets all found metadata fields as metadataFieldSuggestions
|
||||
* @param query The query to look for
|
||||
*/
|
||||
findMetadataFieldSuggestions(query: string): void {
|
||||
this.metadataFieldService.queryMetadataFields(query).pipe(
|
||||
// getSucceededRemoteData(),
|
||||
take(1),
|
||||
map((data) => data.payload.page)
|
||||
).subscribe(
|
||||
(fields: MetadataField[]) => this.metadataFieldSuggestions.next(
|
||||
fields.map((field: MetadataField) => {
|
||||
return {
|
||||
displayValue: field.toString(),
|
||||
value: field.toString()
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user should be allowed to edit this field
|
||||
* @return an observable that emits true when the user should be able to edit this field and false when they should not
|
||||
*/
|
||||
canSetEditable(): Observable<boolean> {
|
||||
return this.editable.pipe(
|
||||
map((editable: boolean) => {
|
||||
if (editable) {
|
||||
return false;
|
||||
} else {
|
||||
return this.fieldUpdate.changeType !== FieldChangeType.REMOVE;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user should be allowed to disabled editing this field
|
||||
* @return an observable that emits true when the user should be able to disable editing this field and false when they should not
|
||||
*/
|
||||
canSetUneditable(): Observable<boolean> {
|
||||
return this.editable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user should be allowed to remove this field
|
||||
* @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> {
|
||||
return this.editable.pipe(
|
||||
map((editable: boolean) => {
|
||||
if (editable) {
|
||||
return false;
|
||||
} else {
|
||||
return this.fieldUpdate.changeType !== FieldChangeType.REMOVE && this.fieldUpdate.changeType !== FieldChangeType.ADD;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user should be allowed to undo changes to this field
|
||||
* @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> {
|
||||
return observableOf(this.fieldUpdate.changeType >= 0);
|
||||
}
|
||||
|
||||
protected isNotEmpty(value): boolean {
|
||||
return isNotEmpty(value);
|
||||
}
|
||||
|
||||
update() {
|
||||
this.mdUpdate.emit();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.mdRemove.emit()
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,55 @@
|
||||
<div class="container">
|
||||
<div class="item-metadata">
|
||||
<button class="btn btn-primary w-100 my-2" (click)="update()">{{"item.edit.metadata.add-button" | translate}}</button>
|
||||
<table class="table table-responsive table-striped">
|
||||
<div class="button-row d-flex justify-content-between">
|
||||
<button class="btn btn-success my-2"
|
||||
(click)="add()"><i
|
||||
class="fas fa-plus"></i> {{"item.edit.metadata.add-button" | translate}}
|
||||
</button>
|
||||
|
||||
<div class="btn-group btn-group-toggle my-2" data-toggle="buttons">
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
|
||||
(click)="submit()"><i
|
||||
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-responsive table-striped table-bordered">
|
||||
<tbody>
|
||||
<tr *ngFor="let metadatum of item.metadata; let i=index">
|
||||
<ds-edit-in-place-field [metadata]="metadatum" class="d-flex" (mdUpdate)="update()" (mdRemove)="update()"></ds-edit-in-place-field>
|
||||
<tr class="d-flex">
|
||||
<th class="col-3">{{'item.edit.metadata.headers.field' | translate}}</th>
|
||||
<th class="col-7">{{'item.edit.metadata.headers.value' | translate}}</th>
|
||||
<th class="col-1 text-center">{{'item.edit.metadata.headers.language' | translate}}</th>
|
||||
<th class="col-1 text-center">{{'item.edit.metadata.headers.edit' | translate}}</th>
|
||||
</tr>
|
||||
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate">
|
||||
<ds-edit-in-place-field [fieldUpdate]="updateValue || {}"
|
||||
[route]="route"></ds-edit-in-place-field>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="btn-group btn-group-toggle my-2 float-right" data-toggle="buttons">
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
|
||||
(click)="submit()"><i
|
||||
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,199 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { ItemMetadataComponent } from './item-metadata.component';
|
||||
import { Metadatum } from '../../../core/shared/metadatum.model';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { SharedModule } from '../../../shared/shared.module';
|
||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import {
|
||||
INotification,
|
||||
Notification
|
||||
} from '../../../shared/notifications/models/notification.model';
|
||||
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
||||
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||
import { GLOBAL_CONFIG } from '../../../../config';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
|
||||
let comp: ItemMetadataComponent;
|
||||
let fixture: ComponentFixture<ItemMetadataComponent>;
|
||||
let de: DebugElement;
|
||||
let el: HTMLElement;
|
||||
let objectUpdatesService;
|
||||
const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info');
|
||||
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
|
||||
const date = new Date();
|
||||
const router = new RouterStub();
|
||||
let itemService;
|
||||
const notificationsService = jasmine.createSpyObj('notificationsService',
|
||||
{
|
||||
info: infoNotification,
|
||||
warning: warningNotification
|
||||
}
|
||||
);
|
||||
const metadatum1 = Object.assign(new Metadatum(), {
|
||||
key: 'dc.description.abstract',
|
||||
value: 'Example abstract',
|
||||
language: 'en'
|
||||
});
|
||||
|
||||
const metadatum2 = Object.assign(new Metadatum(), {
|
||||
key: 'dc.title',
|
||||
value: 'Title test',
|
||||
language: 'de'
|
||||
});
|
||||
|
||||
const metadatum3 = Object.assign(new Metadatum(), {
|
||||
key: 'dc.contributor.author',
|
||||
value: 'Shakespeare, William',
|
||||
});
|
||||
|
||||
const route = 'http://test-url.com/test-url';
|
||||
|
||||
router.url = route;
|
||||
|
||||
const fieldUpdate1 = {
|
||||
field: metadatum1,
|
||||
changeType: undefined
|
||||
};
|
||||
|
||||
const fieldUpdate2 = {
|
||||
field: metadatum2,
|
||||
changeType: FieldChangeType.REMOVE
|
||||
};
|
||||
|
||||
const fieldUpdate3 = {
|
||||
field: metadatum3,
|
||||
changeType: undefined
|
||||
};
|
||||
|
||||
let scheduler: TestScheduler;
|
||||
let item;
|
||||
describe('ItemMetadataComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
item = Object.assign(new Item(), { metadata: [metadatum1, metadatum2, metadatum3] }, { lastModified: date });
|
||||
itemService = jasmine.createSpyObj('itemService', {
|
||||
update: observableOf(new RemoteData(false, false, true, undefined, item))
|
||||
});
|
||||
scheduler = getTestScheduler();
|
||||
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
||||
{
|
||||
getFieldUpdates: observableOf({
|
||||
[metadatum1.uuid]: fieldUpdate1,
|
||||
[metadatum2.uuid]: fieldUpdate2,
|
||||
[metadatum3.uuid]: fieldUpdate3
|
||||
}),
|
||||
saveAddFieldUpdate: {},
|
||||
discardFieldUpdates: {},
|
||||
reinstateFieldUpdates: observableOf(true),
|
||||
initialize: {},
|
||||
getUpdatedFields: observableOf([metadatum1, metadatum2, metadatum3]),
|
||||
getLastModified: observableOf(date),
|
||||
hasUpdates: observableOf(true),
|
||||
isReinstatable: observableOf(false) // should always return something --> its in ngOnInit
|
||||
}
|
||||
);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [SharedModule, TranslateModule.forRoot()],
|
||||
declarations: [ItemMetadataComponent],
|
||||
providers: [
|
||||
{ provide: ItemDataService, useValue: itemService },
|
||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: NotificationsService, useValue: notificationsService },
|
||||
{ provide: GLOBAL_CONFIG, useValue: { notifications: { timeOut: 10 } } as any }
|
||||
], schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
}).compileComponents();
|
||||
})
|
||||
)
|
||||
;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemMetadataComponent);
|
||||
comp = fixture.componentInstance; // EditInPlaceFieldComponent test instance
|
||||
de = fixture.debugElement.query(By.css('div.d-flex'));
|
||||
el = de.nativeElement;
|
||||
comp.item = item;
|
||||
comp.route = route;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('add', () => {
|
||||
const md = new Metadatum();
|
||||
beforeEach(() => {
|
||||
comp.add(md);
|
||||
});
|
||||
|
||||
it('it should call saveAddFieldUpdate on the objectUpdatesService with the correct route and metadata', () => {
|
||||
expect(objectUpdatesService.saveAddFieldUpdate).toHaveBeenCalledWith(route, md);
|
||||
});
|
||||
});
|
||||
|
||||
describe('discard', () => {
|
||||
beforeEach(() => {
|
||||
comp.discard();
|
||||
});
|
||||
|
||||
it('it should call discardFieldUpdates on the objectUpdatesService with the correct route and notification', () => {
|
||||
expect(objectUpdatesService.discardFieldUpdates).toHaveBeenCalledWith(route, infoNotification);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reinstate', () => {
|
||||
beforeEach(() => {
|
||||
comp.reinstate();
|
||||
});
|
||||
|
||||
it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct route', () => {
|
||||
expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(route);
|
||||
});
|
||||
});
|
||||
|
||||
describe('submit', () => {
|
||||
beforeEach(() => {
|
||||
comp.submit();
|
||||
});
|
||||
|
||||
it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct route and metadata', () => {
|
||||
expect(objectUpdatesService.getUpdatedFields).toHaveBeenCalledWith(route, comp.item.metadata);
|
||||
expect(itemService.update).toHaveBeenCalledWith(comp.item);
|
||||
expect(objectUpdatesService.getFieldUpdates).toHaveBeenCalledWith(route, comp.item.metadata);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasChanges', () => {
|
||||
describe('when the objectUpdatesService\'s hasUpdated method returns true', () => {
|
||||
beforeEach(() => {
|
||||
objectUpdatesService.hasUpdates.and.returnValue(observableOf(true));
|
||||
});
|
||||
|
||||
it('should return an observable that emits true', () => {
|
||||
const expected = '(a|)';
|
||||
scheduler.expectObservable(comp.hasChanges()).toBe(expected, { a: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the objectUpdatesService\'s hasUpdated method returns false', () => {
|
||||
beforeEach(() => {
|
||||
objectUpdatesService.hasUpdates.and.returnValue(observableOf(false));
|
||||
});
|
||||
|
||||
it('should return an observable that emits false', () => {
|
||||
const expected = '(a|)';
|
||||
scheduler.expectObservable(comp.hasChanges()).toBe(expected, { a: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
;
|
@@ -1,40 +1,165 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
FieldUpdate,
|
||||
FieldUpdates,
|
||||
Identifiable
|
||||
} from '../../../core/data/object-updates/object-updates.reducer';
|
||||
import { Metadatum } from '../../../core/shared/metadatum.model';
|
||||
import { DSOChangeAnalyzer } from '../../../core/data/dso-change-analyzer.service';
|
||||
import { first, switchMap } from 'rxjs/operators';
|
||||
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-metadata',
|
||||
templateUrl: './item-metadata.component.html',
|
||||
})
|
||||
/**
|
||||
* Component for displaying an item's status
|
||||
* Component for displaying an item's metadata edit page
|
||||
*/
|
||||
export class ItemMetadataComponent {
|
||||
export class ItemMetadataComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The item to display the metadata for
|
||||
* The item to display the edit page for
|
||||
*/
|
||||
@Input() item: Item;
|
||||
updateItem: Item;
|
||||
constructor(private itemService: ItemDataService, private dsoChanges: DSOChangeAnalyzer<Item>) {
|
||||
this.updateItem = Object.assign({}, this.item);
|
||||
/**
|
||||
* The current values and updates for all this item's metadata fields
|
||||
*/
|
||||
updates$: Observable<FieldUpdates>;
|
||||
/**
|
||||
* The current route of this page
|
||||
*/
|
||||
route: string;
|
||||
/**
|
||||
* The time span for being able to undo discarding changes
|
||||
*/
|
||||
private discardTimeOut: number;
|
||||
|
||||
constructor(
|
||||
private itemService: ItemDataService,
|
||||
private objectUpdatesService: ObjectUpdatesService,
|
||||
private router: Router,
|
||||
private notificationsService: NotificationsService,
|
||||
private translateService: TranslateService,
|
||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
update() {
|
||||
this.dsoChanges.diff(this.item, this.updateItem);
|
||||
ngOnInit(): void {
|
||||
this.discardTimeOut = this.EnvConfig.notifications.timeOut;
|
||||
this.route = this.router.url;
|
||||
if (this.route.indexOf('?') > 0) {
|
||||
this.route = this.route.substr(0, this.route.indexOf('?'));
|
||||
}
|
||||
this.hasChanges().pipe(first()).subscribe((hasChanges) => {
|
||||
if (!hasChanges) {
|
||||
this.initializeOriginalFields();
|
||||
} else {
|
||||
this.checkLastModified();
|
||||
}
|
||||
});
|
||||
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.route, this.item.metadata);
|
||||
}
|
||||
|
||||
// submit() {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// discard() {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// undo() {
|
||||
//
|
||||
// }
|
||||
/**
|
||||
* Sends a new add update for a field to the object updates service
|
||||
* @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum
|
||||
*/
|
||||
add(metadata: Metadatum = new Metadatum()) {
|
||||
this.objectUpdatesService.saveAddFieldUpdate(this.route, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the object updates service to discard all current changes to this item
|
||||
* Shows a notification to remind the user that they can undo this
|
||||
*/
|
||||
discard() {
|
||||
const title = this.translateService.instant('item.edit.metadata.notifications.discarded.title');
|
||||
const content = this.translateService.instant('item.edit.metadata.notifications.discarded.content');
|
||||
const undoNotification = this.notificationsService.info(title, content, { timeOut: this.discardTimeOut });
|
||||
this.objectUpdatesService.discardFieldUpdates(this.route, undoNotification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the object updates service to undo discarding all changes to this item
|
||||
*/
|
||||
reinstate() {
|
||||
this.objectUpdatesService.reinstateFieldUpdates(this.route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends all initial values of this item to the object updates service
|
||||
*/
|
||||
private initializeOriginalFields() {
|
||||
this.objectUpdatesService.initialize(this.route, this.item.metadata, this.item.lastModified);
|
||||
}
|
||||
|
||||
/* Prevent unnecessary rerendering so fields don't lose focus **/
|
||||
protected trackUpdate(index, update: FieldUpdate) {
|
||||
return update && update.field ? update.field.uuid : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests all current metadata for this item and requests the item service to update the item
|
||||
* Makes sure the new version of the item is rendered on the page
|
||||
*/
|
||||
submit() {
|
||||
const metadata$: Observable<Identifiable[]> = this.objectUpdatesService.getUpdatedFields(this.route, this.item.metadata) as Observable<Metadatum[]>;
|
||||
metadata$.pipe(
|
||||
first(),
|
||||
switchMap((metadata: Metadatum[]) => {
|
||||
const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata });
|
||||
return this.itemService.update(updatedItem);
|
||||
}),
|
||||
getSucceededRemoteData()
|
||||
).subscribe(
|
||||
(rd: RemoteData<Item>) => {
|
||||
this.item = rd.payload;
|
||||
this.initializeOriginalFields();
|
||||
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.route, this.item.metadata);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not there are currently updates for this item
|
||||
*/
|
||||
hasChanges(): Observable<boolean> {
|
||||
return this.objectUpdatesService.hasUpdates(this.route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not the item is currently reinstatable
|
||||
*/
|
||||
isReinstatable(): Observable<boolean> {
|
||||
return this.objectUpdatesService.isReinstatable(this.route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current item is still in sync with the version in the store
|
||||
* If it's not, a notification is shown and the changes are removed
|
||||
*/
|
||||
private checkLastModified() {
|
||||
const currentVersion = this.item.lastModified;
|
||||
this.objectUpdatesService.getLastModified(this.route).pipe(first()).subscribe(
|
||||
(updateVersion: Date) => {
|
||||
if (updateVersion.getDate() !== currentVersion.getDate()) {
|
||||
const title = this.translateService.instant('item.edit.metadata.notifications.outdated.title');
|
||||
const content = this.translateService.instant('item.edit.metadata.notifications.outdated.content');
|
||||
this.notificationsService.warning(title, content);
|
||||
this.initializeOriginalFields();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -4,9 +4,9 @@ import { RouterModule } from '@angular/router';
|
||||
import { ItemPageComponent } from './simple/item-page.component';
|
||||
import { FullItemPageComponent } from './full/full-item-page.component';
|
||||
import { ItemPageResolver } from './item-page.resolver';
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
import { getItemModulePath } from '../app-routing.module';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import {URLCombiner} from '../core/url-combiner/url-combiner';
|
||||
import {getItemModulePath} from '../app-routing.module';
|
||||
|
||||
export function getItemPageRoute(itemId: string) {
|
||||
return new URLCombiner(getItemModulePath(), itemId).toString();
|
||||
|
@@ -21,6 +21,7 @@ import { SearchService } from '../../../search-service/search.service';
|
||||
import { FILTER_CONFIG, SearchFilterService } from '../search-filter.service';
|
||||
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
||||
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-facet-filter',
|
||||
@@ -59,7 +60,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Emits the result values for this filter found by the current filter query
|
||||
*/
|
||||
filterSearchResults: Observable<any[]> = observableOf([]);
|
||||
filterSearchResults: Observable<InputSuggestion[]> = observableOf([]);
|
||||
|
||||
/**
|
||||
* Emits the active values for this filter
|
||||
@@ -266,7 +267,10 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||
map(
|
||||
(rd: RemoteData<PaginatedList<FacetValue>>) => {
|
||||
return rd.payload.page.map((facet) => {
|
||||
return { displayValue: this.getDisplayValue(facet, data), value: facet.value }
|
||||
return {
|
||||
displayValue: this.getDisplayValue(facet, data),
|
||||
value: facet.value
|
||||
}
|
||||
})
|
||||
}
|
||||
))
|
||||
|
@@ -1,4 +1,3 @@
|
||||
|
||||
import { autoserialize, autoserializeAs } from 'cerialize';
|
||||
|
||||
/**
|
||||
|
@@ -50,13 +50,13 @@ export class RemoteDataBuildService {
|
||||
const payload$ =
|
||||
observableCombineLatest(
|
||||
href$.pipe(
|
||||
switchMap((href: string) => this.objectCache.getBySelfLink<NormalizedObject<T>>(href)),
|
||||
switchMap((href: string) => this.objectCache.getBySelfLink<T>(href)),
|
||||
startWith(undefined)),
|
||||
requestEntry$.pipe(
|
||||
getResourceLinksFromResponse(),
|
||||
switchMap((resourceSelfLinks: string[]) => {
|
||||
if (isNotEmpty(resourceSelfLinks)) {
|
||||
return this.objectCache.getBySelfLink(resourceSelfLinks[0]);
|
||||
return this.objectCache.getBySelfLink<T>(resourceSelfLinks[0]);
|
||||
} else {
|
||||
return observableOf(undefined);
|
||||
}
|
||||
|
@@ -26,7 +26,6 @@ export interface ServerSyncBufferState {
|
||||
buffer: ServerSyncBufferEntry[];
|
||||
}
|
||||
|
||||
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||
const initialState: ServerSyncBufferState = { buffer: [] };
|
||||
|
||||
/**
|
||||
|
@@ -4,11 +4,13 @@ import { UUIDIndexEffects } from './index/index.effects';
|
||||
import { RequestEffects } from './data/request.effects';
|
||||
import { AuthEffects } from './auth/auth.effects';
|
||||
import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
|
||||
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
|
||||
|
||||
export const coreEffects = [
|
||||
RequestEffects,
|
||||
ObjectCacheEffects,
|
||||
UUIDIndexEffects,
|
||||
AuthEffects,
|
||||
ServerSyncBufferEffects
|
||||
ServerSyncBufferEffects,
|
||||
ObjectUpdatesEffects
|
||||
];
|
||||
|
@@ -67,6 +67,7 @@ import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { NormalizedObjectBuildService } from './cache/builders/normalized-object-build.service';
|
||||
import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service';
|
||||
import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
|
||||
|
||||
const IMPORTS = [
|
||||
CommonModule,
|
||||
@@ -134,6 +135,7 @@ const PROVIDERS = [
|
||||
DSOChangeAnalyzer,
|
||||
CSSVariableService,
|
||||
MenuService,
|
||||
ObjectUpdatesService,
|
||||
// register AuthInterceptor as HttpInterceptor
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
|
@@ -1,14 +1,26 @@
|
||||
import { ActionReducerMap, createFeatureSelector } from '@ngrx/store';
|
||||
import {
|
||||
ActionReducerMap,
|
||||
createFeatureSelector,
|
||||
createSelector,
|
||||
MemoizedSelector
|
||||
} from '@ngrx/store';
|
||||
|
||||
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
|
||||
import { indexReducer, IndexState } from './index/index.reducer';
|
||||
import { requestReducer, RequestState } from './data/request.reducer';
|
||||
import { authReducer, AuthState } from './auth/auth.reducer';
|
||||
import { serverSyncBufferReducer, ServerSyncBufferState } from './cache/server-sync-buffer.reducer';
|
||||
import {
|
||||
objectUpdatesReducer,
|
||||
ObjectUpdatesState
|
||||
} from './data/object-updates/object-updates.reducer';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { AppState } from '../app.reducer';
|
||||
|
||||
export interface CoreState {
|
||||
'cache/object': ObjectCacheState,
|
||||
'cache/syncbuffer': ServerSyncBufferState,
|
||||
'cache/object-updates': ObjectUpdatesState
|
||||
'data/request': RequestState,
|
||||
'index': IndexState,
|
||||
'auth': AuthState,
|
||||
@@ -17,9 +29,10 @@ export interface CoreState {
|
||||
export const coreReducers: ActionReducerMap<CoreState> = {
|
||||
'cache/object': objectCacheReducer,
|
||||
'cache/syncbuffer': serverSyncBufferReducer,
|
||||
'cache/object-updates': objectUpdatesReducer,
|
||||
'data/request': requestReducer,
|
||||
'index': indexReducer,
|
||||
'auth': authReducer
|
||||
'auth': authReducer,
|
||||
};
|
||||
|
||||
export const coreSelector = createFeatureSelector<CoreState>('core');
|
||||
export const coreSelector = createFeatureSelector<CoreState>('core');
|
@@ -1,73 +1,86 @@
|
||||
import { type } from '../../../shared/ngrx/type';
|
||||
import { Action } from '@ngrx/store';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { Identifiable } from './object-updates.reducer';
|
||||
import { INotification } from '../../../shared/notifications/models/notification.model';
|
||||
|
||||
export const ObjectUpdatesActionTypes = {
|
||||
ADD: type('dspace/core/object-updates/ADD'),
|
||||
APPLY: type('dspace/core/object-updates/APPLY'),
|
||||
DISCARD: type('dspace/core/object-updates/DISCARD'),
|
||||
REINSTATE: type('dspace/core/object-updates/REINSTATE'),
|
||||
REMOVE: type('dspace/core/object-updates/REMOVE'),
|
||||
REMOVE_SINGLE: type('dspace/core/object-updates/REMOVE_SINGLE'),
|
||||
REPLACE: type('dspace/core/object-updates/REPLACE')
|
||||
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'),
|
||||
SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
|
||||
ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'),
|
||||
DISCARD: type('dspace/core/cache/object-updates/DISCARD'),
|
||||
REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
|
||||
REMOVE: type('dspace/core/cache/object-updates/REMOVE'),
|
||||
REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD'),
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
export enum FieldChangeType {
|
||||
UPDATE = 0,
|
||||
ADD = 1,
|
||||
REMOVE = 2
|
||||
}
|
||||
|
||||
export class ReplaceObjectUpdatesAction implements Action {
|
||||
type = ObjectUpdatesActionTypes.REPLACE;
|
||||
export class InitializeFieldsAction implements Action {
|
||||
type = ObjectUpdatesActionTypes.INITIALIZE_FIELDS;
|
||||
payload: {
|
||||
url: string,
|
||||
operations: Operation[],
|
||||
lastModified: number
|
||||
fields: Identifiable[],
|
||||
lastModified: Date
|
||||
};
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
operations: Operation[],
|
||||
lastModified: number
|
||||
fields: Identifiable[],
|
||||
lastModified: Date
|
||||
) {
|
||||
this.payload = { url, operations, lastModified };
|
||||
this.payload = { url, fields, lastModified };
|
||||
}
|
||||
}
|
||||
|
||||
export class AddToObjectUpdatesAction implements Action {
|
||||
type = ObjectUpdatesActionTypes.ADD;
|
||||
export class AddFieldUpdateAction implements Action {
|
||||
type = ObjectUpdatesActionTypes.ADD_FIELD;
|
||||
payload: {
|
||||
url: string,
|
||||
operation: Operation
|
||||
field: Identifiable,
|
||||
changeType: FieldChangeType,
|
||||
};
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
operation: Operation) {
|
||||
this.payload = { url, operation };
|
||||
field: Identifiable,
|
||||
changeType: FieldChangeType) {
|
||||
this.payload = { url, field, changeType };
|
||||
}
|
||||
}
|
||||
|
||||
export class ApplyObjectUpdatesAction implements Action {
|
||||
type = ObjectUpdatesActionTypes.APPLY;
|
||||
export class SetEditableFieldUpdateAction implements Action {
|
||||
type = ObjectUpdatesActionTypes.SET_EDITABLE_FIELD;
|
||||
payload: {
|
||||
url: string
|
||||
url: string,
|
||||
uuid: string,
|
||||
editable: boolean,
|
||||
};
|
||||
|
||||
constructor(
|
||||
url: string
|
||||
) {
|
||||
this.payload.url = url;
|
||||
url: string,
|
||||
fieldUUID: string,
|
||||
editable: boolean) {
|
||||
this.payload = { url, uuid: fieldUUID, editable };
|
||||
}
|
||||
}
|
||||
|
||||
export class DiscardObjectUpdatesAction implements Action {
|
||||
type = ObjectUpdatesActionTypes.DISCARD;
|
||||
payload: {
|
||||
url: string
|
||||
url: string,
|
||||
notification
|
||||
};
|
||||
|
||||
constructor(
|
||||
url: string
|
||||
url: string,
|
||||
notification: INotification
|
||||
) {
|
||||
this.payload.url = url;
|
||||
this.payload = { url, notification };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +93,7 @@ export class ReinstateObjectUpdatesAction implements Action {
|
||||
constructor(
|
||||
url: string
|
||||
) {
|
||||
this.payload.url = url;
|
||||
this.payload = { url };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,35 +106,34 @@ export class RemoveObjectUpdatesAction implements Action {
|
||||
constructor(
|
||||
url: string
|
||||
) {
|
||||
this.payload.url = url;
|
||||
this.payload = { url };
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveSingleObjectUpdateAction implements Action {
|
||||
type = ObjectUpdatesActionTypes.REMOVE_SINGLE;
|
||||
export class RemoveFieldUpdateAction implements Action {
|
||||
type = ObjectUpdatesActionTypes.REMOVE_FIELD;
|
||||
payload: {
|
||||
url: string,
|
||||
fieldID: string
|
||||
uuid: string
|
||||
};
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
fieldID: string
|
||||
uuid: string
|
||||
) {
|
||||
this.payload = { url, fieldID };
|
||||
this.payload = { url, uuid };
|
||||
}
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* A type to encompass all RequestActions
|
||||
* A type to encompass all ObjectUpdatesActions
|
||||
*/
|
||||
export type ObjectUpdatesAction
|
||||
= AddToObjectUpdatesAction
|
||||
| ApplyObjectUpdatesAction
|
||||
= AddFieldUpdateAction
|
||||
| InitializeFieldsAction
|
||||
| DiscardObjectUpdatesAction
|
||||
| ReinstateObjectUpdatesAction
|
||||
| RemoveObjectUpdatesAction
|
||||
| RemoveSingleObjectUpdateAction
|
||||
| ReplaceObjectUpdatesAction;
|
||||
| RemoveFieldUpdateAction;
|
||||
|
@@ -0,0 +1,55 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Observable } from 'rxjs';
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { cold, hot } from 'jasmine-marbles';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { ObjectUpdatesEffects } from './object-updates.effects';
|
||||
import { RemoveObjectUpdatesAction } from './object-updates.actions';
|
||||
|
||||
fdescribe('ObjectUpdatesEffects', () => {
|
||||
let updatesEffects: ObjectUpdatesEffects;
|
||||
let actions: Observable<any>;
|
||||
const testURL = 'www.dspace.org/dspace7';
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ObjectUpdatesEffects,
|
||||
provideMockActions(() => actions),
|
||||
{
|
||||
provide: NotificationsService, useClass: {
|
||||
remove: (notification) => { /* empty */
|
||||
}
|
||||
}
|
||||
},
|
||||
// other providers
|
||||
],
|
||||
});
|
||||
|
||||
updatesEffects = TestBed.get(ObjectUpdatesEffects);
|
||||
});
|
||||
|
||||
describe('mapLastActions$', () => {
|
||||
describe('When any ObjectUpdatesAction is triggered', () => {
|
||||
const action = new RemoveObjectUpdatesAction(testURL);
|
||||
it('should emit the action from the actionMap\'s value which key matches the action\'s URL', () => {
|
||||
actions = hot('--a-', { a: action });
|
||||
|
||||
const expected = cold('--b-', { b: action });
|
||||
|
||||
expect((updatesEffects as any).actionMap[testURL]).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// describe('removeAfterDiscardOrReinstateOnUndo$', () => {
|
||||
//
|
||||
// it('should return a COLLAPSE action in response to an UPDATE_LOCATION action', () => {
|
||||
// actions = hot('--a-', { a: { type: fromRouter.ROUTER_NAVIGATION } });
|
||||
//
|
||||
// const expected = cold('--b-', { b: new CollapseMenuAction(MenuID.PUBLIC) });
|
||||
//
|
||||
// expect(updatesEffects.routeChange$).toBeObservable(expected);
|
||||
// });
|
||||
//
|
||||
// });
|
||||
});
|
63
src/app/core/data/object-updates/object-updates.effects.ts
Normal file
63
src/app/core/data/object-updates/object-updates.effects.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||
import {
|
||||
DiscardObjectUpdatesAction,
|
||||
ObjectUpdatesAction,
|
||||
ObjectUpdatesActionTypes
|
||||
} from './object-updates.actions';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Subject } from 'rxjs';
|
||||
import { hasNoValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectUpdatesEffects {
|
||||
private actionMap: {
|
||||
/* Use Subject instead of BehaviorSubject:
|
||||
we only want Actions that are fired while we're listening
|
||||
actions that were previously fired do not matter anymore
|
||||
*/
|
||||
[url: string]: Subject<ObjectUpdatesAction>
|
||||
} = {};
|
||||
|
||||
@Effect({ dispatch: false }) mapLastActions$ = this.actions$
|
||||
.pipe(
|
||||
ofType(...Object.values(ObjectUpdatesActionTypes)),
|
||||
map((action: DiscardObjectUpdatesAction) => {
|
||||
const url: string = action.payload.url;
|
||||
if (hasNoValue(this.actionMap[url])) {
|
||||
this.actionMap[url] = new Subject<ObjectUpdatesAction>();
|
||||
}
|
||||
this.actionMap[url].next(action);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// @Effect() removeAfterDiscardOrReinstateOnUndo$ = this.actions$
|
||||
// .pipe(
|
||||
// ofType(ObjectUpdatesActionTypes.DISCARD),
|
||||
// switchMap((action: DiscardObjectUpdatesAction) => {
|
||||
// const url: string = action.payload.url;
|
||||
// const notification: INotification = action.payload.notification;
|
||||
// const timeOut = notification.options.timeOut;
|
||||
// return observableRace(
|
||||
// // Either wait for the delay and perform a remove action
|
||||
// observableOf(new RemoveObjectUpdatesAction(action.payload.url)).pipe(delay(timeOut)),
|
||||
// // Or wait for a reinstate action and perform no action
|
||||
// this.actionMap[url].pipe(
|
||||
// // filter((updateAction: ObjectUpdatesAction) => updateAction.type === ObjectUpdatesActionTypes.REINSTATE),
|
||||
// tap(() => this.notificationsService.remove(notification)),
|
||||
// map(() => {
|
||||
// return { type: 'NO_ACTION' }
|
||||
// }
|
||||
// )
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// )
|
||||
// );
|
||||
|
||||
constructor(private actions$: Actions, private notificationsService: NotificationsService) {
|
||||
}
|
||||
|
||||
}
|
@@ -1,45 +1,66 @@
|
||||
import {
|
||||
AddToObjectUpdatesAction,
|
||||
ApplyObjectUpdatesAction,
|
||||
AddFieldUpdateAction,
|
||||
DiscardObjectUpdatesAction,
|
||||
FieldChangeType,
|
||||
InitializeFieldsAction,
|
||||
ObjectUpdatesAction,
|
||||
ObjectUpdatesActionTypes,
|
||||
ReinstateObjectUpdatesAction,
|
||||
RemoveObjectUpdatesAction,
|
||||
RemoveSingleObjectUpdateAction, ReplaceObjectUpdatesAction
|
||||
RemoveFieldUpdateAction,
|
||||
RemoveObjectUpdatesAction, SetEditableFieldUpdateAction
|
||||
} from './object-updates.actions';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||
|
||||
export const OBJECT_UPDATES_TRASH_PATH = '/trash';
|
||||
|
||||
export interface FieldState {
|
||||
editable: boolean,
|
||||
isNew: boolean
|
||||
}
|
||||
|
||||
export interface FieldStates {
|
||||
[uuid: string]: FieldState;
|
||||
}
|
||||
|
||||
export interface Identifiable {
|
||||
uuid: string
|
||||
}
|
||||
|
||||
export interface FieldUpdate {
|
||||
field: Identifiable,
|
||||
changeType: FieldChangeType
|
||||
}
|
||||
|
||||
export interface FieldUpdates {
|
||||
[uuid: string]: FieldUpdate;
|
||||
}
|
||||
|
||||
export interface ObjectUpdatesEntry {
|
||||
updates: Operation[];
|
||||
lastServerUpdate: number;
|
||||
lastModified: number;
|
||||
discarded: boolean;
|
||||
fieldStates: FieldStates;
|
||||
fieldUpdates: FieldUpdates
|
||||
lastModified: Date;
|
||||
// lastUpdate: Date;
|
||||
}
|
||||
|
||||
export interface ObjectUpdatesState {
|
||||
[url: string]: ObjectUpdatesEntry;
|
||||
}
|
||||
|
||||
const initialFieldState = { editable: false, isNew: false };
|
||||
const initialNewFieldState = { editable: true, isNew: true };
|
||||
|
||||
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||
const initialState = Object.create(null);
|
||||
|
||||
export function objectUpdatesReducer(state = initialState, action: ObjectUpdatesAction): ObjectUpdatesState {
|
||||
let newState = state;
|
||||
switch (action.type) {
|
||||
case ObjectUpdatesActionTypes.REPLACE: {
|
||||
newState = replaceObjectUpdates(state, action as ReplaceObjectUpdatesAction);
|
||||
case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: {
|
||||
newState = initializeFieldsUpdate(state, action as InitializeFieldsAction);
|
||||
break;
|
||||
}
|
||||
case ObjectUpdatesActionTypes.ADD: {
|
||||
newState = addToObjectUpdates(state, action as AddToObjectUpdatesAction);
|
||||
break;
|
||||
}
|
||||
case ObjectUpdatesActionTypes.APPLY: {
|
||||
/* For now do nothing, handle in effect */
|
||||
// return applyObjectUpdates(state, action as ApplyObjectUpdatesAction);
|
||||
newState = state;
|
||||
case ObjectUpdatesActionTypes.ADD_FIELD: {
|
||||
newState = addFieldUpdate(state, action as AddFieldUpdateAction);
|
||||
break;
|
||||
}
|
||||
case ObjectUpdatesActionTypes.DISCARD: {
|
||||
@@ -54,86 +75,162 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
|
||||
newState = removeObjectUpdates(state, action as RemoveObjectUpdatesAction);
|
||||
break;
|
||||
}
|
||||
case ObjectUpdatesActionTypes.REMOVE_SINGLE: {
|
||||
newState = removeSingleObjectUpdates(state, action as RemoveSingleObjectUpdateAction);
|
||||
case ObjectUpdatesActionTypes.REMOVE_FIELD: {
|
||||
newState = removeFieldUpdate(state, action as RemoveFieldUpdateAction);
|
||||
break;
|
||||
}
|
||||
case ObjectUpdatesActionTypes.SET_EDITABLE_FIELD: {
|
||||
// return directly, no need to change the lastModified date
|
||||
return setEditableFieldUpdate(state, action as SetEditableFieldUpdateAction);
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
return setLastModified(newState, action.payload.url);
|
||||
}
|
||||
|
||||
function replaceObjectUpdates(state: any, action: ReplaceObjectUpdatesAction) {
|
||||
const key: string = action.payload.url;
|
||||
const operations: Operation[] = action.payload.operations;
|
||||
const newUpdateEntry = Object.assign({}, state[key] || {}, { updates: operations });
|
||||
return Object.assign({}, state, { [key]: newUpdateEntry });
|
||||
}
|
||||
|
||||
function addToObjectUpdates(state: any, action: AddToObjectUpdatesAction) {
|
||||
const key: string = action.payload.url;
|
||||
const operation: Operation = action.payload.operation;
|
||||
const keyState = state[key] || {
|
||||
updates: {},
|
||||
lastServerUpdate: 0,
|
||||
discarded: false
|
||||
};
|
||||
const objectUpdates: Operation[] = keyState.updates || [];
|
||||
const newUpdates = [...objectUpdates, operation];
|
||||
const newKeyState = Object.assign({}, state[key], { updates: newUpdates });
|
||||
return Object.assign({}, state, newKeyState);
|
||||
}
|
||||
|
||||
function discardObjectUpdates(state: any, action: DiscardObjectUpdatesAction) {
|
||||
const key: string = action.payload.url;
|
||||
const keyState = state[key];
|
||||
if (hasValue(keyState)) {
|
||||
const newKeyState = Object.assign({}, keyState, { discarded: true });
|
||||
return Object.assign({}, state, newKeyState);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function reinstateObjectUpdates(state: any, action: ReinstateObjectUpdatesAction) {
|
||||
const key: string = action.payload.url;
|
||||
const keyState = state[key];
|
||||
if (hasValue(keyState)) {
|
||||
const newKeyState = Object.assign({}, keyState, { discarded: false });
|
||||
return Object.assign({}, state, newKeyState);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function removeObjectUpdates(state: any, action: RemoveObjectUpdatesAction) {
|
||||
const key: string = action.payload.url;
|
||||
return removeObjectUpdatesByURL(state, key);
|
||||
}
|
||||
|
||||
function removeObjectUpdatesByURL(state: any, url: string) {
|
||||
const keyState = state[url];
|
||||
const newState = Object.assign({}, state);
|
||||
if (hasValue(keyState)) {
|
||||
delete newState[url];
|
||||
}
|
||||
// return setUpdated(newState, action.payload.url);
|
||||
return newState;
|
||||
}
|
||||
|
||||
function removeSingleObjectUpdates(state: any, action: RemoveSingleObjectUpdateAction) {
|
||||
const key: string = action.payload.url;
|
||||
let newKeyState = state[key];
|
||||
if (hasValue(newKeyState)) {
|
||||
const newUpdates: Operation[] = Object.assign({}, newKeyState.updates);
|
||||
if (hasValue(newUpdates[action.payload.fieldID])) {
|
||||
delete newUpdates[action.payload.fieldID];
|
||||
}
|
||||
newKeyState = Object.assign({}, state[key], { updates: newUpdates });
|
||||
}
|
||||
return Object.assign({}, state, newKeyState);
|
||||
function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
|
||||
const url: string = action.payload.url;
|
||||
const fields: Identifiable[] = action.payload.fields;
|
||||
const lastModifiedServer: Date = action.payload.lastModified;
|
||||
const fieldStates = createInitialFieldStates(fields);
|
||||
const newPageState = Object.assign(
|
||||
{},
|
||||
state[url],
|
||||
{ fieldStates: fieldStates },
|
||||
{ fieldUpdates: {} },
|
||||
{ lastServerUpdate: lastModifiedServer }
|
||||
);
|
||||
return Object.assign({}, state, { [url]: newPageState });
|
||||
}
|
||||
|
||||
function setLastModified(state: any, url: string) {
|
||||
const newKeyState = Object.assign({}, state[url] || {}, { lastModified: Date.now() });
|
||||
return Object.assign({}, state, newKeyState);
|
||||
function addFieldUpdate(state: any, action: AddFieldUpdateAction) {
|
||||
const url: string = action.payload.url;
|
||||
const field: Identifiable = action.payload.field;
|
||||
const changeType: FieldChangeType = action.payload.changeType;
|
||||
const pageState: ObjectUpdatesEntry = state[url] || {};
|
||||
|
||||
let states = pageState.fieldStates;
|
||||
if (changeType === FieldChangeType.ADD) {
|
||||
states = Object.assign({}, { [field.uuid]: initialNewFieldState }, pageState.fieldStates)
|
||||
}
|
||||
|
||||
let fieldUpdate: any = pageState.fieldUpdates[field.uuid] || {};
|
||||
const newChangeType = determineChangeType(fieldUpdate.changeType, changeType);
|
||||
|
||||
fieldUpdate = Object.assign({}, { field, changeType: newChangeType });
|
||||
|
||||
const fieldUpdates = Object.assign({}, pageState.fieldUpdates, { [field.uuid]: fieldUpdate });
|
||||
|
||||
const newPageState = Object.assign({}, pageState,
|
||||
{ fieldStates: states },
|
||||
{ fieldUpdates: fieldUpdates });
|
||||
return Object.assign({}, state, { [url]: newPageState });
|
||||
}
|
||||
|
||||
function discardObjectUpdates(state: any, action: DiscardObjectUpdatesAction) {
|
||||
const url: string = action.payload.url;
|
||||
const pageState: ObjectUpdatesEntry = state[url];
|
||||
const newFieldStates = {};
|
||||
Object.keys(pageState.fieldStates).forEach((uuid: string) => {
|
||||
const fieldState: FieldState = pageState.fieldStates[uuid];
|
||||
if (!fieldState.isNew) {
|
||||
/* After discarding we don't want the reset fields to stay editable */
|
||||
newFieldStates[uuid] = Object.assign({}, fieldState, { editable: false });
|
||||
}
|
||||
});
|
||||
|
||||
const discardedPageState = Object.assign({}, pageState, {
|
||||
fieldUpdates: {},
|
||||
fieldStates: newFieldStates
|
||||
});
|
||||
return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState });
|
||||
}
|
||||
|
||||
function reinstateObjectUpdates(state: any, action: ReinstateObjectUpdatesAction) {
|
||||
const url: string = action.payload.url;
|
||||
const trashState = state[url + OBJECT_UPDATES_TRASH_PATH];
|
||||
|
||||
const newState = Object.assign({}, state, { [url]: trashState });
|
||||
delete newState[url + OBJECT_UPDATES_TRASH_PATH];
|
||||
return newState;
|
||||
}
|
||||
|
||||
function removeObjectUpdates(state: any, action: RemoveObjectUpdatesAction) {
|
||||
const url: string = action.payload.url;
|
||||
return removeObjectUpdatesByURL(state, url);
|
||||
}
|
||||
|
||||
function removeObjectUpdatesByURL(state: any, url: string) {
|
||||
const newState = Object.assign({}, state);
|
||||
delete newState[url + OBJECT_UPDATES_TRASH_PATH];
|
||||
return newState;
|
||||
}
|
||||
|
||||
function removeFieldUpdate(state: any, action: RemoveFieldUpdateAction) {
|
||||
const url: string = action.payload.url;
|
||||
const uuid: string = action.payload.uuid;
|
||||
let newPageState: ObjectUpdatesEntry = state[url];
|
||||
if (hasValue(newPageState)) {
|
||||
const newUpdates: FieldUpdates = Object.assign({}, newPageState.fieldUpdates);
|
||||
if (hasValue(newUpdates[uuid])) {
|
||||
delete newUpdates[uuid];
|
||||
}
|
||||
const newFieldStates: FieldStates = Object.assign({}, newPageState.fieldStates);
|
||||
if (hasValue(newFieldStates[uuid])) {
|
||||
/* When resetting, make field not editable */
|
||||
if (newFieldStates[uuid].isNew) {
|
||||
/* If this field was added, just throw it away */
|
||||
delete newFieldStates[uuid];
|
||||
} else {
|
||||
newFieldStates[uuid] = Object.assign({}, newFieldStates[uuid], { editable: false });
|
||||
}
|
||||
}
|
||||
newPageState = Object.assign({}, state[url], {
|
||||
fieldUpdates: newUpdates,
|
||||
fieldStates: newFieldStates
|
||||
});
|
||||
}
|
||||
return Object.assign({}, state, { [url]: newPageState });
|
||||
}
|
||||
|
||||
function setUpdated(state: any, url: string) {
|
||||
const newPageState = Object.assign({}, state[url] || {}, { lastUpdated: Date.now() });
|
||||
return Object.assign({}, state, { [url]: newPageState });
|
||||
}
|
||||
|
||||
function determineChangeType(oldType: FieldChangeType, newType: FieldChangeType): FieldChangeType {
|
||||
if (hasNoValue(newType)) {
|
||||
return oldType;
|
||||
}
|
||||
if (hasNoValue(oldType)) {
|
||||
return newType;
|
||||
}
|
||||
return oldType.valueOf() > newType.valueOf() ? oldType : newType;
|
||||
}
|
||||
|
||||
function setEditableFieldUpdate(state: any, action: SetEditableFieldUpdateAction) {
|
||||
const url: string = action.payload.url;
|
||||
const uuid: string = action.payload.uuid;
|
||||
const editable: boolean = action.payload.editable;
|
||||
|
||||
const pageState: ObjectUpdatesEntry = state[url];
|
||||
|
||||
const fieldState = pageState.fieldStates[uuid];
|
||||
const newFieldState = Object.assign({}, fieldState, { editable });
|
||||
|
||||
const newFieldStates = Object.assign({}, pageState.fieldStates, { [uuid]: newFieldState });
|
||||
|
||||
const newPageState = Object.assign({}, pageState, { fieldStates: newFieldStates });
|
||||
|
||||
return Object.assign({}, state, { [url]: newPageState });
|
||||
}
|
||||
|
||||
function createInitialFieldStates(fields: Identifiable[]) {
|
||||
const uuids = fields.map((field: Identifiable) => field.uuid);
|
||||
const fieldStates = {};
|
||||
uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState);
|
||||
return fieldStates;
|
||||
}
|
||||
|
134
src/app/core/data/object-updates/object-updates.service.ts
Normal file
134
src/app/core/data/object-updates/object-updates.service.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { coreSelector, CoreState } from '../../core.reducers';
|
||||
import {
|
||||
FieldUpdates,
|
||||
Identifiable, OBJECT_UPDATES_TRASH_PATH,
|
||||
ObjectUpdatesEntry,
|
||||
ObjectUpdatesState
|
||||
} from './object-updates.reducer';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
AddFieldUpdateAction,
|
||||
DiscardObjectUpdatesAction,
|
||||
FieldChangeType,
|
||||
InitializeFieldsAction,
|
||||
ReinstateObjectUpdatesAction,
|
||||
RemoveFieldUpdateAction,
|
||||
SetEditableFieldUpdateAction
|
||||
} from './object-updates.actions';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { INotification } from '../../../shared/notifications/models/notification.model';
|
||||
|
||||
function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> {
|
||||
return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']);
|
||||
}
|
||||
|
||||
function filterByUrlObjectUpdatesStateSelector(url: string): MemoizedSelector<CoreState, ObjectUpdatesEntry> {
|
||||
return createSelector(objectUpdatesStateSelector(), (state: ObjectUpdatesState) => state[url]);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ObjectUpdatesService {
|
||||
constructor(private store: Store<CoreState>) {
|
||||
|
||||
}
|
||||
|
||||
initialize(url, fields: Identifiable[], lastModified: Date): void {
|
||||
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified));
|
||||
}
|
||||
|
||||
private saveFieldUpdate(url: string, field: Identifiable, changeType: FieldChangeType) {
|
||||
this.store.dispatch(new AddFieldUpdateAction(url, field, changeType))
|
||||
}
|
||||
|
||||
private getObjectEntry(url: string): Observable<ObjectUpdatesEntry> {
|
||||
return this.store.pipe(select(filterByUrlObjectUpdatesStateSelector(url)));
|
||||
}
|
||||
|
||||
getFieldUpdates(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
|
||||
const objectUpdates = this.getObjectEntry(url);
|
||||
return objectUpdates.pipe(map((objectEntry) => {
|
||||
const fieldUpdates: FieldUpdates = {};
|
||||
Object.keys(objectEntry.fieldStates).forEach((uuid) => {
|
||||
let fieldUpdate = objectEntry.fieldUpdates[uuid];
|
||||
if (isEmpty(fieldUpdate)) {
|
||||
const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid);
|
||||
fieldUpdate = { field: identifiable, changeType: undefined };
|
||||
}
|
||||
fieldUpdates[uuid] = fieldUpdate;
|
||||
});
|
||||
return fieldUpdates;
|
||||
}))
|
||||
}
|
||||
|
||||
isEditable(url: string, uuid: string): Observable<boolean> {
|
||||
const objectUpdates = this.getObjectEntry(url);
|
||||
return objectUpdates.pipe(
|
||||
filter((objectEntry) => hasValue(objectEntry.fieldStates[uuid])),
|
||||
map((objectEntry) => objectEntry.fieldStates[uuid].editable
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
saveAddFieldUpdate(url: string, field: Identifiable) {
|
||||
this.saveFieldUpdate(url, field, FieldChangeType.ADD);
|
||||
}
|
||||
|
||||
saveRemoveFieldUpdate(url: string, field: Identifiable) {
|
||||
this.saveFieldUpdate(url, field, FieldChangeType.REMOVE);
|
||||
}
|
||||
|
||||
saveChangeFieldUpdate(url: string, field: Identifiable) {
|
||||
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
|
||||
}
|
||||
|
||||
setEditableFieldUpdate(url: string, uuid: string, editable: boolean) {
|
||||
this.store.dispatch(new SetEditableFieldUpdateAction(url, uuid, editable));
|
||||
}
|
||||
|
||||
discardFieldUpdates(url: string, undoNotification: INotification) {
|
||||
this.store.dispatch(new DiscardObjectUpdatesAction(url, undoNotification));
|
||||
}
|
||||
|
||||
reinstateFieldUpdates(url: string) {
|
||||
this.store.dispatch(new ReinstateObjectUpdatesAction(url));
|
||||
}
|
||||
|
||||
removeSingleFieldUpdate(url: string, uuid) {
|
||||
this.store.dispatch(new RemoveFieldUpdateAction(url, uuid));
|
||||
}
|
||||
|
||||
getUpdatedFields(url: string, initialFields: Identifiable[]): Observable<Identifiable[]> {
|
||||
const objectUpdates = this.getObjectEntry(url);
|
||||
return objectUpdates.pipe(map((objectEntry) => {
|
||||
const fields: Identifiable[] = [];
|
||||
Object.keys(objectEntry.fieldStates).forEach((uuid) => {
|
||||
const fieldUpdate = objectEntry.fieldUpdates[uuid];
|
||||
if (hasNoValue(fieldUpdate) || fieldUpdate.changeType !== FieldChangeType.REMOVE) {
|
||||
let field;
|
||||
if (isNotEmpty(fieldUpdate)) {
|
||||
field = fieldUpdate.field;
|
||||
} else {
|
||||
field = initialFields.find((object: Identifiable) => object.uuid === uuid);
|
||||
}
|
||||
fields.push(field);
|
||||
}
|
||||
});
|
||||
return fields;
|
||||
}))
|
||||
}
|
||||
|
||||
hasUpdates(url: string): Observable<boolean> {
|
||||
return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && isNotEmpty(objectEntry.fieldUpdates)));
|
||||
}
|
||||
|
||||
isReinstatable(route: string): Observable<boolean> {
|
||||
return this.hasUpdates(route + OBJECT_UPDATES_TRASH_PATH)
|
||||
}
|
||||
|
||||
getLastModified(url: string): Observable<Date> {
|
||||
return this.getObjectEntry(url).pipe(map((entry: ObjectUpdatesEntry) => entry.lastModified));
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
import { MetadataSchema } from './metadataschema.model';
|
||||
import { autoserialize } from 'cerialize';
|
||||
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
|
||||
export class MetadataField implements ListableObject {
|
||||
@autoserialize
|
||||
@@ -20,4 +21,12 @@ export class MetadataField implements ListableObject {
|
||||
|
||||
@autoserialize
|
||||
schema: MetadataSchema;
|
||||
|
||||
toString(): string {
|
||||
let key = this.schema.prefix + '.' + this.element;
|
||||
if (isNotEmpty(this.qualifier)) {
|
||||
key += '.' + this.qualifier;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
@@ -33,13 +33,19 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { RegistryBitstreamformatsResponseParsingService } from '../data/registry-bitstreamformats-response-parsing.service';
|
||||
import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model';
|
||||
import { configureRequest, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
|
||||
import {
|
||||
configureRequest,
|
||||
getResponseFromEntry,
|
||||
getSucceededRemoteData
|
||||
} from '../shared/operators';
|
||||
import { createSelector, select, Store } from '@ngrx/store';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers';
|
||||
import {
|
||||
MetadataRegistryCancelFieldAction,
|
||||
MetadataRegistryCancelSchemaAction, MetadataRegistryDeselectAllFieldAction, MetadataRegistryDeselectAllSchemaAction,
|
||||
MetadataRegistryCancelSchemaAction,
|
||||
MetadataRegistryDeselectAllFieldAction,
|
||||
MetadataRegistryDeselectAllSchemaAction,
|
||||
MetadataRegistryDeselectFieldAction,
|
||||
MetadataRegistryDeselectSchemaAction,
|
||||
MetadataRegistryEditFieldAction,
|
||||
@@ -168,7 +174,7 @@ export class RegistryService {
|
||||
|
||||
public getAllMetadataFields(pagination?: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||
if (hasNoValue(pagination)) {
|
||||
pagination = { currentPage: 1, pageSize: Number.MAX_VALUE } as any;
|
||||
pagination = { currentPage: 1, pageSize: 10000 } as any;
|
||||
}
|
||||
const requestObs = this.getMetadataFieldsRequestObs(pagination);
|
||||
|
||||
@@ -533,4 +539,19 @@ export class RegistryService {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
queryMetadataFields(query: string): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||
/**
|
||||
* This should come directly from the server in the future
|
||||
*/
|
||||
return this.getAllMetadataFields().pipe(
|
||||
map((rd: RemoteData<PaginatedList<MetadataField>>) => {
|
||||
const filteredFields: MetadataField[] = rd.payload.page.filter(
|
||||
(field: MetadataField) => field.toString().indexOf(query) >= 0
|
||||
);
|
||||
const page: PaginatedList<MetadataField> = new PaginatedList<MetadataField>(new PageInfo(), filteredFields)
|
||||
return Object.assign({}, rd, { payload: page });
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { autoserialize } from 'cerialize';
|
||||
import * as uuidv4 from 'uuid/v4';
|
||||
|
||||
export class Metadatum {
|
||||
|
||||
uuid: string = uuidv4();
|
||||
/**
|
||||
* The metadata field of this Metadatum
|
||||
*/
|
||||
|
@@ -1,21 +1,21 @@
|
||||
<form #form="ngForm" (ngSubmit)="submitSuggestion.emit(ngModel)"
|
||||
<form #form="ngForm" (ngSubmit)="onSubmit(value)"
|
||||
[action]="action" (keydown)="onKeydown($event)"
|
||||
(keydown.arrowdown)="shiftFocusDown($event)"
|
||||
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
|
||||
(dsClickOutside)="close()">
|
||||
<input #inputField type="text" [(ngModel)]="ngModel" [name]="name"
|
||||
<input #inputField type="text" [(ngModel)]="value" [name]="name"
|
||||
class="form-control suggestion_input"
|
||||
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
|
||||
[placeholder]="placeholder"
|
||||
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
|
||||
<input type="submit" class="d-none"/>
|
||||
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
||||
<ul class="list-unstyled">
|
||||
<li *ngFor="let suggestionOption of suggestions">
|
||||
<div>
|
||||
<div *ngFor="let suggestionOption of suggestions">
|
||||
<a href="#" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption.value)" #suggestion>
|
||||
<span [innerHTML]="suggestionOption.displayValue"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@@ -1,8 +1,11 @@
|
||||
@import "../../../styles/_variables.scss";
|
||||
|
||||
.autocomplete {
|
||||
width: 100%;
|
||||
.dropdown-item {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef, EventEmitter,
|
||||
ElementRef, EventEmitter, forwardRef,
|
||||
Input,
|
||||
Output,
|
||||
QueryList, SimpleChanges,
|
||||
@@ -9,21 +9,30 @@ import {
|
||||
} from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { hasValue, isNotEmpty } from '../empty.util';
|
||||
import { InputSuggestion } from './input-suggestions.model';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-input-suggestions',
|
||||
styleUrls: ['./input-suggestions.component.scss'],
|
||||
templateUrl: './input-suggestions.component.html'
|
||||
templateUrl: './input-suggestions.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => InputSuggestionsComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* Component representing a form with a autocomplete functionality
|
||||
*/
|
||||
export class InputSuggestionsComponent {
|
||||
export class InputSuggestionsComponent implements ControlValueAccessor {
|
||||
/**
|
||||
* The suggestions that should be shown
|
||||
*/
|
||||
@Input() suggestions: any[] = [];
|
||||
@Input() suggestions: InputSuggestion[] = [];
|
||||
|
||||
/**
|
||||
* The time waited to detect if any other input will follow before requesting the suggestions
|
||||
@@ -45,16 +54,6 @@ export class InputSuggestionsComponent {
|
||||
*/
|
||||
@Input() name;
|
||||
|
||||
/**
|
||||
* Value of the input field
|
||||
*/
|
||||
@Input() ngModel;
|
||||
|
||||
/**
|
||||
* Output for when the input field's value changes
|
||||
*/
|
||||
@Output() ngModelChange = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Output for when the form is submitted
|
||||
*/
|
||||
@@ -94,6 +93,15 @@ export class InputSuggestionsComponent {
|
||||
*/
|
||||
@ViewChildren('suggestion') resultViews: QueryList<ElementRef>;
|
||||
|
||||
/**
|
||||
* Value of the input field
|
||||
*/
|
||||
_value: string;
|
||||
|
||||
propagateChange = (_: any) => {
|
||||
/* Empty implementation */
|
||||
};
|
||||
|
||||
/**
|
||||
* When any of the inputs change, check if we should still show the suggestions
|
||||
*/
|
||||
@@ -170,6 +178,7 @@ export class InputSuggestionsComponent {
|
||||
* Make sure that if a suggestion is clicked, the suggestions dropdown closes, does not reopen and the focus moves to the input field
|
||||
*/
|
||||
onClickSuggestion(data) {
|
||||
this.value = data;
|
||||
this.clickSuggestion.emit(data);
|
||||
this.close();
|
||||
this.blockReopen = true;
|
||||
@@ -188,4 +197,31 @@ export class InputSuggestionsComponent {
|
||||
this.blockReopen = false;
|
||||
}
|
||||
|
||||
onSubmit(data) {
|
||||
this.value = data;
|
||||
this.submitSuggestion.emit(data);
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this.propagateChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
}
|
||||
|
||||
writeValue(value: any): void {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
set value(val) {
|
||||
this._value = val;
|
||||
this.propagateChange(this._value);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,4 @@
|
||||
export interface InputSuggestion {
|
||||
displayValue: string,
|
||||
value: string
|
||||
}
|
@@ -27,27 +27,30 @@ export class NotificationsService {
|
||||
|
||||
success(title: any = observableOf(''),
|
||||
content: any = observableOf(''),
|
||||
options: NotificationOptions = this.getDefaultOptions(),
|
||||
options: Partial<NotificationOptions> = {},
|
||||
html: boolean = false): INotification {
|
||||
const notification = new Notification(uniqueId(), NotificationType.Success, title, content, options, html);
|
||||
const notificationOptions = { ...this.getDefaultOptions(), ...options };
|
||||
const notification = new Notification(uniqueId(), NotificationType.Success, title, content, notificationOptions, html);
|
||||
this.add(notification);
|
||||
return notification;
|
||||
}
|
||||
|
||||
error(title: any = observableOf(''),
|
||||
content: any = observableOf(''),
|
||||
options: NotificationOptions = this.getDefaultOptions(),
|
||||
options: Partial<NotificationOptions> = {},
|
||||
html: boolean = false): INotification {
|
||||
const notification = new Notification(uniqueId(), NotificationType.Error, title, content, options, html);
|
||||
const notificationOptions = { ...this.getDefaultOptions(), ...options };
|
||||
const notification = new Notification(uniqueId(), NotificationType.Error, title, content, notificationOptions, html);
|
||||
this.add(notification);
|
||||
return notification;
|
||||
}
|
||||
|
||||
info(title: any = observableOf(''),
|
||||
content: any = observableOf(''),
|
||||
options: NotificationOptions = this.getDefaultOptions(),
|
||||
options: Partial<NotificationOptions> = {},
|
||||
html: boolean = false): INotification {
|
||||
const notification = new Notification(uniqueId(), NotificationType.Info, title, content, options, html);
|
||||
const notificationOptions = { ...this.getDefaultOptions(), ...options };
|
||||
const notification = new Notification(uniqueId(), NotificationType.Info, title, content, notificationOptions, html);
|
||||
this.add(notification);
|
||||
return notification;
|
||||
}
|
||||
@@ -56,7 +59,8 @@ export class NotificationsService {
|
||||
content: any = observableOf(''),
|
||||
options: NotificationOptions = this.getDefaultOptions(),
|
||||
html: boolean = false): INotification {
|
||||
const notification = new Notification(uniqueId(), NotificationType.Warning, title, content, options, html);
|
||||
const notificationOptions = { ...this.getDefaultOptions(), ...options };
|
||||
const notification = new Notification(uniqueId(), NotificationType.Warning, title, content, notificationOptions, html);
|
||||
this.add(notification);
|
||||
return notification;
|
||||
}
|
||||
|
@@ -91,6 +91,7 @@ import { CreateComColPageComponent } from './comcol-forms/create-comcol-page/cre
|
||||
import { EditComColPageComponent } from './comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||
import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
|
||||
import { ObjectValuesPipe } from './utils/object-values-pipe';
|
||||
|
||||
const MODULES = [
|
||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||
@@ -124,6 +125,7 @@ const PIPES = [
|
||||
EmphasizePipe,
|
||||
CapitalizePipe,
|
||||
ObjectKeysPipe,
|
||||
ObjectValuesPipe,
|
||||
ConsolePipe
|
||||
];
|
||||
|
||||
|
18
src/app/shared/utils/object-values-pipe.ts
Normal file
18
src/app/shared/utils/object-values-pipe.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { PipeTransform, Pipe } from '@angular/core';
|
||||
|
||||
@Pipe({name: 'dsObjectValues'})
|
||||
/**
|
||||
* Pipe for parsing all values of an object to an array of values
|
||||
*/
|
||||
export class ObjectValuesPipe implements PipeTransform {
|
||||
|
||||
/**
|
||||
* @param value An object
|
||||
* @returns {any} Array with all values of the input object
|
||||
*/
|
||||
transform(value, args:string[]): any {
|
||||
const values = [];
|
||||
Object.values(value).forEach((v) => values.push(v));
|
||||
return values;
|
||||
}
|
||||
}
|
@@ -24,7 +24,7 @@ $gray-100: lighten($gray-base, 93.5%) !default; // #eee
|
||||
$blue: #2B4E72 !default;
|
||||
$green: #94BA65 !default;
|
||||
$cyan: #2790B0 !default;
|
||||
$yellow: #EBBB54 !default;
|
||||
$yellow: #ec9433 !default;
|
||||
$red: #CF4444 !default;
|
||||
$dark: darken($blue, 17%) !default;
|
||||
|
||||
@@ -56,3 +56,4 @@ $grid-breakpoints: (
|
||||
xl: (1200px - $collapsed-sidebar-width)
|
||||
) !default;
|
||||
|
||||
$yiq-contrasted-threshold: 165 !default;
|
||||
|
Reference in New Issue
Block a user