mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 02:24:11 +00:00
intermediate commit for tests
This commit is contained in:
@@ -79,5 +79,8 @@ module.exports = {
|
|||||||
code: 'nl',
|
code: 'nl',
|
||||||
label: 'Nederlands',
|
label: 'Nederlands',
|
||||||
active: false,
|
active: false,
|
||||||
}]
|
}],
|
||||||
|
item: {
|
||||||
|
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@@ -223,7 +223,26 @@
|
|||||||
"error": "An error occured while deleting the item"
|
"error": "An error occured while deleting the item"
|
||||||
},
|
},
|
||||||
"metadata": {
|
"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>
|
</ng-template>
|
||||||
</ngb-tab>
|
</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>
|
<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>
|
</ng-template>
|
||||||
</ngb-tab>
|
</ngb-tab>
|
||||||
<ngb-tab title="{{'item.edit.tabs.view.head' | translate}}">
|
<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 {ItemPublicComponent} from './item-public/item-public.component';
|
||||||
import {ItemDeleteComponent} from './item-delete/item-delete.component';
|
import {ItemDeleteComponent} from './item-delete/item-delete.component';
|
||||||
import { ItemMetadataComponent } from './item-metadata/item-metadata.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
|
* 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,
|
ItemDeleteComponent,
|
||||||
ItemStatusComponent,
|
ItemStatusComponent,
|
||||||
ItemMetadataComponent,
|
ItemMetadataComponent,
|
||||||
EditInPlaceComponent
|
EditInPlaceFieldComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class EditItemPageModule {
|
export class EditItemPageModule {
|
||||||
|
@@ -1,42 +1,47 @@
|
|||||||
<td class="col-3">
|
<div [ngClass]="{
|
||||||
<!--<div *ngIf="!editable">-->
|
'table-warning': fieldUpdate.changeType === 0,
|
||||||
<span>{{metadata.key}}</span>
|
'table-danger': fieldUpdate.changeType === 2,
|
||||||
<!--</div>-->
|
'table-success': fieldUpdate.changeType === 1
|
||||||
<!--<div *ngIf="editable" class="field-container">-->
|
}" class="d-flex">
|
||||||
<!--<ds-input-suggestions [suggestions]="(filterSearchResults | async)"-->
|
<!--{{metadata?.uuid}}-->
|
||||||
<!--[action]="getCurrentUrl()"-->
|
<td class="col-3">
|
||||||
<!--[name]="filterConfig.paramName"-->
|
<div *ngIf="!(editable | async)">
|
||||||
<!--[(ngModel)]="metadata.key"-->
|
<span>{{metadata?.key}}</span>
|
||||||
<!--(submitSuggestion)="updateField($event)"-->
|
</div>
|
||||||
<!--(clickSuggestion)="updateField($event)"-->
|
<div *ngIf="(editable | async)" class="field-container">
|
||||||
<!--(findSuggestions)="findSuggestions($event)"-->
|
<ds-input-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
||||||
<!--ngDefaultControl-->
|
[(ngModel)]="metadata.key"
|
||||||
<!--></ds-input-suggestions>-->
|
(submitSuggestion)="update()"
|
||||||
<!--</div>-->
|
(clickSuggestion)="update()"
|
||||||
</td>
|
(findSuggestions)="findMetadataFieldSuggestions($event)"
|
||||||
<td class="col-7">
|
ngDefaultControl
|
||||||
<div *ngIf="!editable">
|
></ds-input-suggestions>
|
||||||
<span>{{metadata.value}}</span>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
<div *ngIf="editable" class="field-container">
|
<td class="col-7">
|
||||||
<textarea type="textarea" [ngModel]="metadata.value" [dsDebounce] (onDebounce)="update()"></textarea>
|
<div *ngIf="!(editable | async)">
|
||||||
</div>
|
<span>{{metadata?.value}}</span>
|
||||||
</td>
|
</div>
|
||||||
<td class="col-1">
|
<div *ngIf="(editable | async)" class="field-container">
|
||||||
<div *ngIf="!editable">
|
<textarea class="form-control" type="textarea" [(ngModel)]="metadata.value" [dsDebounce]
|
||||||
<span>{{metadata.language}}</span>
|
(onDebounce)="update()"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="editable" class="field-container">
|
</td>
|
||||||
<input type="text" [ngModel]="metadata.language" [dsDebounce] (onDebounce)="update()"/>
|
<td class="col-1 text-center">
|
||||||
</div>
|
<div *ngIf="!(editable | async)">
|
||||||
</td>
|
<span>{{metadata?.language}}</span>
|
||||||
<td class="col-1">
|
</div>
|
||||||
<div *ngIf="!editable">
|
<div *ngIf="(editable | async)" class="field-container">
|
||||||
<i class="fas fa-edit fa-fw" (click)="editable = !editable"></i>
|
<input class="form-control" type="text" [(ngModel)]="metadata.language" [dsDebounce]
|
||||||
<i class="fas fa-trash fa-fw" (click)="remove()"></i>
|
(onDebounce)="update()"/>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="editable">
|
</td>
|
||||||
<i class="fas fa-times fa-fw" (click)="editable = !editable"></i>
|
<td class="col-1 text-center">
|
||||||
<i class="fas fa-trash fa-fw" (click)="remove()"></i>
|
<div>
|
||||||
</div>
|
<i *ngIf="canSetEditable() | async" class="fas fa-edit fa-fw text-primary" (click)="setEditable(true)"></i>
|
||||||
</td>
|
<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 { isNotEmpty } from '../../../../shared/empty.util';
|
||||||
import { Metadatum } from '../../../../core/shared/metadatum.model';
|
import { Metadatum } from '../../../../core/shared/metadatum.model';
|
||||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
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({
|
@Component({
|
||||||
selector: 'ds-edit-in-place-field.',
|
selector: 'ds-edit-in-place-field',
|
||||||
styleUrls: ['./edit-in-place-field.component.scss'],
|
styleUrls: ['./edit-in-place-field.component.scss'],
|
||||||
templateUrl: './edit-in-place-field.component.html',
|
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;
|
@Input() fieldUpdate: FieldUpdate;
|
||||||
@Output() mdUpdate: EventEmitter<any> = new EventEmitter();
|
/**
|
||||||
@Output() mdRemove: EventEmitter<any> = new EventEmitter();
|
* The current route of this page
|
||||||
editable = false;
|
*/
|
||||||
|
@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(
|
constructor(
|
||||||
private metadataFieldService: RegistryService,
|
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);
|
return isNotEmpty(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
|
||||||
this.mdUpdate.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
this.mdRemove.emit()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,55 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="item-metadata">
|
<div class="item-metadata">
|
||||||
<button class="btn btn-primary w-100 my-2" (click)="update()">{{"item.edit.metadata.add-button" | translate}}</button>
|
<div class="button-row d-flex justify-content-between">
|
||||||
<table class="table table-responsive table-striped">
|
<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>
|
<tbody>
|
||||||
<tr *ngFor="let metadatum of item.metadata; let i=index">
|
<tr class="d-flex">
|
||||||
<ds-edit-in-place-field [metadata]="metadatum" class="d-flex" (mdUpdate)="update()" (mdRemove)="update()"></ds-edit-in-place-field>
|
<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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
||||||
</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 { 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 { 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 { 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({
|
@Component({
|
||||||
selector: 'ds-item-metadata',
|
selector: 'ds-item-metadata',
|
||||||
templateUrl: './item-metadata.component.html',
|
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;
|
@Input() item: Item;
|
||||||
updateItem: Item;
|
/**
|
||||||
constructor(private itemService: ItemDataService, private dsoChanges: DSOChangeAnalyzer<Item>) {
|
* The current values and updates for all this item's metadata fields
|
||||||
this.updateItem = Object.assign({}, this.item);
|
*/
|
||||||
|
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() {
|
ngOnInit(): void {
|
||||||
this.dsoChanges.diff(this.item, this.updateItem);
|
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() {
|
/**
|
||||||
//
|
* 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
|
||||||
//
|
*/
|
||||||
// discard() {
|
add(metadata: Metadatum = new Metadatum()) {
|
||||||
//
|
this.objectUpdatesService.saveAddFieldUpdate(this.route, metadata);
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// undo() {
|
/**
|
||||||
//
|
* 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 { ItemPageComponent } from './simple/item-page.component';
|
||||||
import { FullItemPageComponent } from './full/full-item-page.component';
|
import { FullItemPageComponent } from './full/full-item-page.component';
|
||||||
import { ItemPageResolver } from './item-page.resolver';
|
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 { 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) {
|
export function getItemPageRoute(itemId: string) {
|
||||||
return new URLCombiner(getItemModulePath(), itemId).toString();
|
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 { FILTER_CONFIG, SearchFilterService } from '../search-filter.service';
|
||||||
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
||||||
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
|
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-facet-filter',
|
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
|
* 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
|
* Emits the active values for this filter
|
||||||
@@ -266,7 +267,10 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
map(
|
map(
|
||||||
(rd: RemoteData<PaginatedList<FacetValue>>) => {
|
(rd: RemoteData<PaginatedList<FacetValue>>) => {
|
||||||
return rd.payload.page.map((facet) => {
|
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';
|
import { autoserialize, autoserializeAs } from 'cerialize';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -50,13 +50,13 @@ export class RemoteDataBuildService {
|
|||||||
const payload$ =
|
const payload$ =
|
||||||
observableCombineLatest(
|
observableCombineLatest(
|
||||||
href$.pipe(
|
href$.pipe(
|
||||||
switchMap((href: string) => this.objectCache.getBySelfLink<NormalizedObject<T>>(href)),
|
switchMap((href: string) => this.objectCache.getBySelfLink<T>(href)),
|
||||||
startWith(undefined)),
|
startWith(undefined)),
|
||||||
requestEntry$.pipe(
|
requestEntry$.pipe(
|
||||||
getResourceLinksFromResponse(),
|
getResourceLinksFromResponse(),
|
||||||
switchMap((resourceSelfLinks: string[]) => {
|
switchMap((resourceSelfLinks: string[]) => {
|
||||||
if (isNotEmpty(resourceSelfLinks)) {
|
if (isNotEmpty(resourceSelfLinks)) {
|
||||||
return this.objectCache.getBySelfLink(resourceSelfLinks[0]);
|
return this.objectCache.getBySelfLink<T>(resourceSelfLinks[0]);
|
||||||
} else {
|
} else {
|
||||||
return observableOf(undefined);
|
return observableOf(undefined);
|
||||||
}
|
}
|
||||||
|
@@ -26,7 +26,6 @@ export interface ServerSyncBufferState {
|
|||||||
buffer: ServerSyncBufferEntry[];
|
buffer: ServerSyncBufferEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
|
||||||
const initialState: ServerSyncBufferState = { buffer: [] };
|
const initialState: ServerSyncBufferState = { buffer: [] };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -4,11 +4,13 @@ import { UUIDIndexEffects } from './index/index.effects';
|
|||||||
import { RequestEffects } from './data/request.effects';
|
import { RequestEffects } from './data/request.effects';
|
||||||
import { AuthEffects } from './auth/auth.effects';
|
import { AuthEffects } from './auth/auth.effects';
|
||||||
import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
|
import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
|
||||||
|
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
|
||||||
|
|
||||||
export const coreEffects = [
|
export const coreEffects = [
|
||||||
RequestEffects,
|
RequestEffects,
|
||||||
ObjectCacheEffects,
|
ObjectCacheEffects,
|
||||||
UUIDIndexEffects,
|
UUIDIndexEffects,
|
||||||
AuthEffects,
|
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 { MenuService } from '../shared/menu/menu.service';
|
||||||
import { NormalizedObjectBuildService } from './cache/builders/normalized-object-build.service';
|
import { NormalizedObjectBuildService } from './cache/builders/normalized-object-build.service';
|
||||||
import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service';
|
||||||
|
import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -134,6 +135,7 @@ const PROVIDERS = [
|
|||||||
DSOChangeAnalyzer,
|
DSOChangeAnalyzer,
|
||||||
CSSVariableService,
|
CSSVariableService,
|
||||||
MenuService,
|
MenuService,
|
||||||
|
ObjectUpdatesService,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
{
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
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 { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
|
||||||
import { indexReducer, IndexState } from './index/index.reducer';
|
import { indexReducer, IndexState } from './index/index.reducer';
|
||||||
import { requestReducer, RequestState } from './data/request.reducer';
|
import { requestReducer, RequestState } from './data/request.reducer';
|
||||||
import { authReducer, AuthState } from './auth/auth.reducer';
|
import { authReducer, AuthState } from './auth/auth.reducer';
|
||||||
import { serverSyncBufferReducer, ServerSyncBufferState } from './cache/server-sync-buffer.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 {
|
export interface CoreState {
|
||||||
'cache/object': ObjectCacheState,
|
'cache/object': ObjectCacheState,
|
||||||
'cache/syncbuffer': ServerSyncBufferState,
|
'cache/syncbuffer': ServerSyncBufferState,
|
||||||
|
'cache/object-updates': ObjectUpdatesState
|
||||||
'data/request': RequestState,
|
'data/request': RequestState,
|
||||||
'index': IndexState,
|
'index': IndexState,
|
||||||
'auth': AuthState,
|
'auth': AuthState,
|
||||||
@@ -17,9 +29,10 @@ export interface CoreState {
|
|||||||
export const coreReducers: ActionReducerMap<CoreState> = {
|
export const coreReducers: ActionReducerMap<CoreState> = {
|
||||||
'cache/object': objectCacheReducer,
|
'cache/object': objectCacheReducer,
|
||||||
'cache/syncbuffer': serverSyncBufferReducer,
|
'cache/syncbuffer': serverSyncBufferReducer,
|
||||||
|
'cache/object-updates': objectUpdatesReducer,
|
||||||
'data/request': requestReducer,
|
'data/request': requestReducer,
|
||||||
'index': indexReducer,
|
'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 { type } from '../../../shared/ngrx/type';
|
||||||
import { Action } from '@ngrx/store';
|
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 = {
|
export const ObjectUpdatesActionTypes = {
|
||||||
ADD: type('dspace/core/object-updates/ADD'),
|
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'),
|
||||||
APPLY: type('dspace/core/object-updates/APPLY'),
|
SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
|
||||||
DISCARD: type('dspace/core/object-updates/DISCARD'),
|
ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'),
|
||||||
REINSTATE: type('dspace/core/object-updates/REINSTATE'),
|
DISCARD: type('dspace/core/cache/object-updates/DISCARD'),
|
||||||
REMOVE: type('dspace/core/object-updates/REMOVE'),
|
REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
|
||||||
REMOVE_SINGLE: type('dspace/core/object-updates/REMOVE_SINGLE'),
|
REMOVE: type('dspace/core/cache/object-updates/REMOVE'),
|
||||||
REPLACE: type('dspace/core/object-updates/REPLACE')
|
REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD'),
|
||||||
};
|
};
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
export enum FieldChangeType {
|
||||||
|
UPDATE = 0,
|
||||||
|
ADD = 1,
|
||||||
|
REMOVE = 2
|
||||||
|
}
|
||||||
|
|
||||||
export class ReplaceObjectUpdatesAction implements Action {
|
export class InitializeFieldsAction implements Action {
|
||||||
type = ObjectUpdatesActionTypes.REPLACE;
|
type = ObjectUpdatesActionTypes.INITIALIZE_FIELDS;
|
||||||
payload: {
|
payload: {
|
||||||
url: string,
|
url: string,
|
||||||
operations: Operation[],
|
fields: Identifiable[],
|
||||||
lastModified: number
|
lastModified: Date
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
url: string,
|
url: string,
|
||||||
operations: Operation[],
|
fields: Identifiable[],
|
||||||
lastModified: number
|
lastModified: Date
|
||||||
) {
|
) {
|
||||||
this.payload = { url, operations, lastModified };
|
this.payload = { url, fields, lastModified };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AddToObjectUpdatesAction implements Action {
|
export class AddFieldUpdateAction implements Action {
|
||||||
type = ObjectUpdatesActionTypes.ADD;
|
type = ObjectUpdatesActionTypes.ADD_FIELD;
|
||||||
payload: {
|
payload: {
|
||||||
url: string,
|
url: string,
|
||||||
operation: Operation
|
field: Identifiable,
|
||||||
|
changeType: FieldChangeType,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
url: string,
|
url: string,
|
||||||
operation: Operation) {
|
field: Identifiable,
|
||||||
this.payload = { url, operation };
|
changeType: FieldChangeType) {
|
||||||
|
this.payload = { url, field, changeType };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApplyObjectUpdatesAction implements Action {
|
export class SetEditableFieldUpdateAction implements Action {
|
||||||
type = ObjectUpdatesActionTypes.APPLY;
|
type = ObjectUpdatesActionTypes.SET_EDITABLE_FIELD;
|
||||||
payload: {
|
payload: {
|
||||||
url: string
|
url: string,
|
||||||
|
uuid: string,
|
||||||
|
editable: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
url: string
|
url: string,
|
||||||
) {
|
fieldUUID: string,
|
||||||
this.payload.url = url;
|
editable: boolean) {
|
||||||
|
this.payload = { url, uuid: fieldUUID, editable };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DiscardObjectUpdatesAction implements Action {
|
export class DiscardObjectUpdatesAction implements Action {
|
||||||
type = ObjectUpdatesActionTypes.DISCARD;
|
type = ObjectUpdatesActionTypes.DISCARD;
|
||||||
payload: {
|
payload: {
|
||||||
url: string
|
url: string,
|
||||||
|
notification
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
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(
|
constructor(
|
||||||
url: string
|
url: string
|
||||||
) {
|
) {
|
||||||
this.payload.url = url;
|
this.payload = { url };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,35 +106,34 @@ export class RemoveObjectUpdatesAction implements Action {
|
|||||||
constructor(
|
constructor(
|
||||||
url: string
|
url: string
|
||||||
) {
|
) {
|
||||||
this.payload.url = url;
|
this.payload = { url };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RemoveSingleObjectUpdateAction implements Action {
|
export class RemoveFieldUpdateAction implements Action {
|
||||||
type = ObjectUpdatesActionTypes.REMOVE_SINGLE;
|
type = ObjectUpdatesActionTypes.REMOVE_FIELD;
|
||||||
payload: {
|
payload: {
|
||||||
url: string,
|
url: string,
|
||||||
fieldID: string
|
uuid: string
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
url: string,
|
url: string,
|
||||||
fieldID: string
|
uuid: string
|
||||||
) {
|
) {
|
||||||
this.payload = { url, fieldID };
|
this.payload = { url, uuid };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A type to encompass all RequestActions
|
* A type to encompass all ObjectUpdatesActions
|
||||||
*/
|
*/
|
||||||
export type ObjectUpdatesAction
|
export type ObjectUpdatesAction
|
||||||
= AddToObjectUpdatesAction
|
= AddFieldUpdateAction
|
||||||
| ApplyObjectUpdatesAction
|
| InitializeFieldsAction
|
||||||
| DiscardObjectUpdatesAction
|
| DiscardObjectUpdatesAction
|
||||||
| ReinstateObjectUpdatesAction
|
| ReinstateObjectUpdatesAction
|
||||||
| RemoveObjectUpdatesAction
|
| RemoveObjectUpdatesAction
|
||||||
| RemoveSingleObjectUpdateAction
|
| RemoveFieldUpdateAction;
|
||||||
| ReplaceObjectUpdatesAction;
|
|
||||||
|
@@ -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 {
|
import {
|
||||||
AddToObjectUpdatesAction,
|
AddFieldUpdateAction,
|
||||||
ApplyObjectUpdatesAction,
|
|
||||||
DiscardObjectUpdatesAction,
|
DiscardObjectUpdatesAction,
|
||||||
|
FieldChangeType,
|
||||||
|
InitializeFieldsAction,
|
||||||
ObjectUpdatesAction,
|
ObjectUpdatesAction,
|
||||||
ObjectUpdatesActionTypes,
|
ObjectUpdatesActionTypes,
|
||||||
ReinstateObjectUpdatesAction,
|
ReinstateObjectUpdatesAction,
|
||||||
RemoveObjectUpdatesAction,
|
RemoveFieldUpdateAction,
|
||||||
RemoveSingleObjectUpdateAction, ReplaceObjectUpdatesAction
|
RemoveObjectUpdatesAction, SetEditableFieldUpdateAction
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||||
import { 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 {
|
export interface ObjectUpdatesEntry {
|
||||||
updates: Operation[];
|
fieldStates: FieldStates;
|
||||||
lastServerUpdate: number;
|
fieldUpdates: FieldUpdates
|
||||||
lastModified: number;
|
lastModified: Date;
|
||||||
discarded: boolean;
|
// lastUpdate: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ObjectUpdatesState {
|
export interface ObjectUpdatesState {
|
||||||
[url: string]: ObjectUpdatesEntry;
|
[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__`)
|
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||||
const initialState = Object.create(null);
|
const initialState = Object.create(null);
|
||||||
|
|
||||||
export function objectUpdatesReducer(state = initialState, action: ObjectUpdatesAction): ObjectUpdatesState {
|
export function objectUpdatesReducer(state = initialState, action: ObjectUpdatesAction): ObjectUpdatesState {
|
||||||
let newState = state;
|
let newState = state;
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ObjectUpdatesActionTypes.REPLACE: {
|
case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: {
|
||||||
newState = replaceObjectUpdates(state, action as ReplaceObjectUpdatesAction);
|
newState = initializeFieldsUpdate(state, action as InitializeFieldsAction);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ObjectUpdatesActionTypes.ADD: {
|
case ObjectUpdatesActionTypes.ADD_FIELD: {
|
||||||
newState = addToObjectUpdates(state, action as AddToObjectUpdatesAction);
|
newState = addFieldUpdate(state, action as AddFieldUpdateAction);
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ObjectUpdatesActionTypes.APPLY: {
|
|
||||||
/* For now do nothing, handle in effect */
|
|
||||||
// return applyObjectUpdates(state, action as ApplyObjectUpdatesAction);
|
|
||||||
newState = state;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ObjectUpdatesActionTypes.DISCARD: {
|
case ObjectUpdatesActionTypes.DISCARD: {
|
||||||
@@ -54,86 +75,162 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
|
|||||||
newState = removeObjectUpdates(state, action as RemoveObjectUpdatesAction);
|
newState = removeObjectUpdates(state, action as RemoveObjectUpdatesAction);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ObjectUpdatesActionTypes.REMOVE_SINGLE: {
|
case ObjectUpdatesActionTypes.REMOVE_FIELD: {
|
||||||
newState = removeSingleObjectUpdates(state, action as RemoveSingleObjectUpdateAction);
|
newState = removeFieldUpdate(state, action as RemoveFieldUpdateAction);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case ObjectUpdatesActionTypes.SET_EDITABLE_FIELD: {
|
||||||
|
// return directly, no need to change the lastModified date
|
||||||
|
return setEditableFieldUpdate(state, action as SetEditableFieldUpdateAction);
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return setLastModified(newState, action.payload.url);
|
// return setUpdated(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 newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeSingleObjectUpdates(state: any, action: RemoveSingleObjectUpdateAction) {
|
function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
|
||||||
const key: string = action.payload.url;
|
const url: string = action.payload.url;
|
||||||
let newKeyState = state[key];
|
const fields: Identifiable[] = action.payload.fields;
|
||||||
if (hasValue(newKeyState)) {
|
const lastModifiedServer: Date = action.payload.lastModified;
|
||||||
const newUpdates: Operation[] = Object.assign({}, newKeyState.updates);
|
const fieldStates = createInitialFieldStates(fields);
|
||||||
if (hasValue(newUpdates[action.payload.fieldID])) {
|
const newPageState = Object.assign(
|
||||||
delete newUpdates[action.payload.fieldID];
|
{},
|
||||||
}
|
state[url],
|
||||||
newKeyState = Object.assign({}, state[key], { updates: newUpdates });
|
{ fieldStates: fieldStates },
|
||||||
}
|
{ fieldUpdates: {} },
|
||||||
return Object.assign({}, state, newKeyState);
|
{ lastServerUpdate: lastModifiedServer }
|
||||||
|
);
|
||||||
|
return Object.assign({}, state, { [url]: newPageState });
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLastModified(state: any, url: string) {
|
function addFieldUpdate(state: any, action: AddFieldUpdateAction) {
|
||||||
const newKeyState = Object.assign({}, state[url] || {}, { lastModified: Date.now() });
|
const url: string = action.payload.url;
|
||||||
return Object.assign({}, state, newKeyState);
|
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 { MetadataSchema } from './metadataschema.model';
|
||||||
import { autoserialize } from 'cerialize';
|
import { autoserialize } from 'cerialize';
|
||||||
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
export class MetadataField implements ListableObject {
|
export class MetadataField implements ListableObject {
|
||||||
@autoserialize
|
@autoserialize
|
||||||
@@ -20,4 +21,12 @@ export class MetadataField implements ListableObject {
|
|||||||
|
|
||||||
@autoserialize
|
@autoserialize
|
||||||
schema: MetadataSchema;
|
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 { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { RegistryBitstreamformatsResponseParsingService } from '../data/registry-bitstreamformats-response-parsing.service';
|
import { RegistryBitstreamformatsResponseParsingService } from '../data/registry-bitstreamformats-response-parsing.service';
|
||||||
import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model';
|
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 { createSelector, select, Store } from '@ngrx/store';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers';
|
import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers';
|
||||||
import {
|
import {
|
||||||
MetadataRegistryCancelFieldAction,
|
MetadataRegistryCancelFieldAction,
|
||||||
MetadataRegistryCancelSchemaAction, MetadataRegistryDeselectAllFieldAction, MetadataRegistryDeselectAllSchemaAction,
|
MetadataRegistryCancelSchemaAction,
|
||||||
|
MetadataRegistryDeselectAllFieldAction,
|
||||||
|
MetadataRegistryDeselectAllSchemaAction,
|
||||||
MetadataRegistryDeselectFieldAction,
|
MetadataRegistryDeselectFieldAction,
|
||||||
MetadataRegistryDeselectSchemaAction,
|
MetadataRegistryDeselectSchemaAction,
|
||||||
MetadataRegistryEditFieldAction,
|
MetadataRegistryEditFieldAction,
|
||||||
@@ -168,7 +174,7 @@ export class RegistryService {
|
|||||||
|
|
||||||
public getAllMetadataFields(pagination?: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
public getAllMetadataFields(pagination?: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||||
if (hasNoValue(pagination)) {
|
if (hasNoValue(pagination)) {
|
||||||
pagination = { currentPage: 1, pageSize: Number.MAX_VALUE } as any;
|
pagination = { currentPage: 1, pageSize: 10000 } as any;
|
||||||
}
|
}
|
||||||
const requestObs = this.getMetadataFieldsRequestObs(pagination);
|
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 { autoserialize } from 'cerialize';
|
||||||
|
import * as uuidv4 from 'uuid/v4';
|
||||||
|
|
||||||
export class Metadatum {
|
export class Metadatum {
|
||||||
|
|
||||||
|
uuid: string = uuidv4();
|
||||||
/**
|
/**
|
||||||
* The metadata field of this Metadatum
|
* 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)"
|
[action]="action" (keydown)="onKeydown($event)"
|
||||||
(keydown.arrowdown)="shiftFocusDown($event)"
|
(keydown.arrowdown)="shiftFocusDown($event)"
|
||||||
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
|
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
|
||||||
(dsClickOutside)="close()">
|
(dsClickOutside)="close()">
|
||||||
<input #inputField type="text" [(ngModel)]="ngModel" [name]="name"
|
<input #inputField type="text" [(ngModel)]="value" [name]="name"
|
||||||
class="form-control suggestion_input"
|
class="form-control suggestion_input"
|
||||||
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
|
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
|
||||||
[placeholder]="placeholder"
|
[placeholder]="placeholder"
|
||||||
[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)}">
|
||||||
<ul class="list-unstyled">
|
<div>
|
||||||
<li *ngFor="let suggestionOption of suggestions">
|
<div *ngFor="let suggestionOption of suggestions">
|
||||||
<a href="#" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption.value)" #suggestion>
|
<a href="#" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption.value)" #suggestion>
|
||||||
<span [innerHTML]="suggestionOption.displayValue"></span>
|
<span [innerHTML]="suggestionOption.displayValue"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
@@ -1,8 +1,11 @@
|
|||||||
|
@import "../../../styles/_variables.scss";
|
||||||
|
|
||||||
.autocomplete {
|
.autocomplete {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
padding: $input-padding-y $input-padding-x;
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
ElementRef, EventEmitter,
|
ElementRef, EventEmitter, forwardRef,
|
||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
QueryList, SimpleChanges,
|
QueryList, SimpleChanges,
|
||||||
@@ -9,21 +9,30 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import { hasValue, isNotEmpty } from '../empty.util';
|
import { hasValue, isNotEmpty } from '../empty.util';
|
||||||
|
import { InputSuggestion } from './input-suggestions.model';
|
||||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-input-suggestions',
|
selector: 'ds-input-suggestions',
|
||||||
styleUrls: ['./input-suggestions.component.scss'],
|
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
|
* Component representing a form with a autocomplete functionality
|
||||||
*/
|
*/
|
||||||
export class InputSuggestionsComponent {
|
export class InputSuggestionsComponent implements ControlValueAccessor {
|
||||||
/**
|
/**
|
||||||
* The suggestions that should be shown
|
* 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
|
* The time waited to detect if any other input will follow before requesting the suggestions
|
||||||
@@ -45,16 +54,6 @@ export class InputSuggestionsComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() name;
|
@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
|
* Output for when the form is submitted
|
||||||
*/
|
*/
|
||||||
@@ -94,6 +93,15 @@ export class InputSuggestionsComponent {
|
|||||||
*/
|
*/
|
||||||
@ViewChildren('suggestion') resultViews: QueryList<ElementRef>;
|
@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
|
* 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
|
* 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) {
|
onClickSuggestion(data) {
|
||||||
|
this.value = data;
|
||||||
this.clickSuggestion.emit(data);
|
this.clickSuggestion.emit(data);
|
||||||
this.close();
|
this.close();
|
||||||
this.blockReopen = true;
|
this.blockReopen = true;
|
||||||
@@ -188,4 +197,31 @@ export class InputSuggestionsComponent {
|
|||||||
this.blockReopen = false;
|
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(''),
|
success(title: any = observableOf(''),
|
||||||
content: any = observableOf(''),
|
content: any = observableOf(''),
|
||||||
options: NotificationOptions = this.getDefaultOptions(),
|
options: Partial<NotificationOptions> = {},
|
||||||
html: boolean = false): INotification {
|
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);
|
this.add(notification);
|
||||||
return notification;
|
return notification;
|
||||||
}
|
}
|
||||||
|
|
||||||
error(title: any = observableOf(''),
|
error(title: any = observableOf(''),
|
||||||
content: any = observableOf(''),
|
content: any = observableOf(''),
|
||||||
options: NotificationOptions = this.getDefaultOptions(),
|
options: Partial<NotificationOptions> = {},
|
||||||
html: boolean = false): INotification {
|
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);
|
this.add(notification);
|
||||||
return notification;
|
return notification;
|
||||||
}
|
}
|
||||||
|
|
||||||
info(title: any = observableOf(''),
|
info(title: any = observableOf(''),
|
||||||
content: any = observableOf(''),
|
content: any = observableOf(''),
|
||||||
options: NotificationOptions = this.getDefaultOptions(),
|
options: Partial<NotificationOptions> = {},
|
||||||
html: boolean = false): INotification {
|
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);
|
this.add(notification);
|
||||||
return notification;
|
return notification;
|
||||||
}
|
}
|
||||||
@@ -56,7 +59,8 @@ export class NotificationsService {
|
|||||||
content: any = observableOf(''),
|
content: any = observableOf(''),
|
||||||
options: NotificationOptions = this.getDefaultOptions(),
|
options: NotificationOptions = this.getDefaultOptions(),
|
||||||
html: boolean = false): INotification {
|
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);
|
this.add(notification);
|
||||||
return 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 { EditComColPageComponent } from './comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||||
import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||||
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
|
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
|
||||||
|
import { ObjectValuesPipe } from './utils/object-values-pipe';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -124,6 +125,7 @@ const PIPES = [
|
|||||||
EmphasizePipe,
|
EmphasizePipe,
|
||||||
CapitalizePipe,
|
CapitalizePipe,
|
||||||
ObjectKeysPipe,
|
ObjectKeysPipe,
|
||||||
|
ObjectValuesPipe,
|
||||||
ConsolePipe
|
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;
|
$blue: #2B4E72 !default;
|
||||||
$green: #94BA65 !default;
|
$green: #94BA65 !default;
|
||||||
$cyan: #2790B0 !default;
|
$cyan: #2790B0 !default;
|
||||||
$yellow: #EBBB54 !default;
|
$yellow: #ec9433 !default;
|
||||||
$red: #CF4444 !default;
|
$red: #CF4444 !default;
|
||||||
$dark: darken($blue, 17%) !default;
|
$dark: darken($blue, 17%) !default;
|
||||||
|
|
||||||
@@ -56,3 +56,4 @@ $grid-breakpoints: (
|
|||||||
xl: (1200px - $collapsed-sidebar-width)
|
xl: (1200px - $collapsed-sidebar-width)
|
||||||
) !default;
|
) !default;
|
||||||
|
|
||||||
|
$yiq-contrasted-threshold: 165 !default;
|
||||||
|
Reference in New Issue
Block a user