mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 02:24:11 +00:00
finalised edit item page
This commit is contained in:
@@ -188,7 +188,7 @@
|
|||||||
"confirm": "Withdraw",
|
"confirm": "Withdraw",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"success": "The item was withdrawn successfully",
|
"success": "The item was withdrawn successfully",
|
||||||
"error": "An error occured while withdrawing the item"
|
"error": "An error occurred while withdrawing the item"
|
||||||
},
|
},
|
||||||
"reinstate": {
|
"reinstate": {
|
||||||
"header": "Reinstate item: {{ id }}",
|
"header": "Reinstate item: {{ id }}",
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
"confirm": "Reinstate",
|
"confirm": "Reinstate",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"success": "The item was reinstated successfully",
|
"success": "The item was reinstated successfully",
|
||||||
"error": "An error occured while reinstating the item"
|
"error": "An error occurred while reinstating the item"
|
||||||
},
|
},
|
||||||
"private": {
|
"private": {
|
||||||
"header": "Make item private: {{ id }}",
|
"header": "Make item private: {{ id }}",
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
"confirm": "Make it Private",
|
"confirm": "Make it Private",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"success": "The item is now private",
|
"success": "The item is now private",
|
||||||
"error": "An error occured while making the item private"
|
"error": "An error occurred while making the item private"
|
||||||
},
|
},
|
||||||
"public": {
|
"public": {
|
||||||
"header": "Make item public: {{ id }}",
|
"header": "Make item public: {{ id }}",
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
"confirm": "Make it Public",
|
"confirm": "Make it Public",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"success": "The item is now public",
|
"success": "The item is now public",
|
||||||
"error": "An error occured while making the item public"
|
"error": "An error occurred while making the item public"
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"header": "Delete item: {{ id }}",
|
"header": "Delete item: {{ id }}",
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
"confirm": "Delete",
|
"confirm": "Delete",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"success": "The item has been deleted",
|
"success": "The item has been deleted",
|
||||||
"error": "An error occured while deleting the item"
|
"error": "An error occurred while deleting the item"
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"add-button": "Add",
|
"add-button": "Add",
|
||||||
@@ -233,6 +233,14 @@
|
|||||||
"language": "Lang",
|
"language": "Lang",
|
||||||
"edit": "Edit"
|
"edit": "Edit"
|
||||||
},
|
},
|
||||||
|
"edit": {
|
||||||
|
"buttons": {
|
||||||
|
"edit": "Edit",
|
||||||
|
"unedit": "Stop editing",
|
||||||
|
"remove": "Remove",
|
||||||
|
"undo": "Undo changes"
|
||||||
|
}
|
||||||
|
},
|
||||||
"metadatafield": {
|
"metadatafield": {
|
||||||
"invalid": "Please choose a valid metadata field"
|
"invalid": "Please choose a valid metadata field"
|
||||||
},
|
},
|
||||||
@@ -247,7 +255,7 @@
|
|||||||
},
|
},
|
||||||
"invalid": {
|
"invalid": {
|
||||||
"title": "Metadata invalid",
|
"title": "Metadata invalid",
|
||||||
"content": "Please make sure all fields are valid"
|
"content": "Your changes were not saved. Please make sure all fields are valid before you save."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,37 +1,21 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h2 class="border-bottom">{{'item.edit.head' | translate}}</h2>
|
<h2 class="border-bottom">{{'item.edit.head' | translate}}</h2>
|
||||||
<div class="pt-2">
|
<div class="pt-2">
|
||||||
<ngb-tabset [activeId]="(params$ | async)?.page || 'status'">
|
<ul class="nav nav-tabs justify-content-start">
|
||||||
<ngb-tab [id]="'status'" title="{{'item.edit.tabs.status.head' | translate}}">
|
<li *ngFor="let page of pages" class="nav-item">
|
||||||
<ng-template ngbTabContent>
|
<a class="nav-link"
|
||||||
<ds-item-status [item]="(itemRD$ | async)?.payload"></ds-item-status>
|
[ngClass]="{'active' : page === currentPage}"
|
||||||
</ng-template>
|
[routerLink]="'/items/' + (itemRD$ | async)?.payload.uuid + '/edit/' + page">
|
||||||
</ngb-tab>
|
{{'item.edit.tabs.' + page + '.head' | translate}}
|
||||||
<ngb-tab [id]="'bitstreams'" title="{{'item.edit.tabs.bitstreams.head' | translate}}">
|
</a>
|
||||||
<ng-template ngbTabContent>
|
</li>
|
||||||
|
</ul>
|
||||||
</ng-template>
|
<div class="tab-pane active">
|
||||||
</ngb-tab>
|
<router-outlet></router-outlet>
|
||||||
<ngb-tab [id]="'metadata'" title="{{'item.edit.tabs.metadata.head' | translate}}">
|
</div>
|
||||||
<ng-template ngbTabContent>
|
</div>
|
||||||
<ds-item-metadata [item]="(itemRD$ | async)?.payload">
|
</div>
|
||||||
</ds-item-metadata>
|
|
||||||
</ng-template>
|
|
||||||
</ngb-tab>
|
|
||||||
<ngb-tab [id]="'view'" title="{{'item.edit.tabs.view.head' | translate}}">
|
|
||||||
<ng-template ngbTabContent>
|
|
||||||
|
|
||||||
</ng-template>
|
|
||||||
</ngb-tab>
|
|
||||||
<ngb-tab [id]="'curate'" title="{{'item.edit.tabs.curate.head' | translate}}">
|
|
||||||
<ng-template ngbTabContent>
|
|
||||||
|
|
||||||
</ng-template>
|
|
||||||
</ngb-tab>
|
|
||||||
</ngb-tabset>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import {fadeIn, fadeInOut} from '../../shared/animations/fade';
|
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||||
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||||
import {RemoteData} from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import {Item} from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import {Observable} from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import {map} from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-edit-item-page',
|
selector: 'ds-edit-item-page',
|
||||||
@@ -24,13 +25,27 @@ export class EditItemPageComponent implements OnInit {
|
|||||||
* The item to edit
|
* The item to edit
|
||||||
*/
|
*/
|
||||||
itemRD$: Observable<RemoteData<Item>>;
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
params$: Observable<Params>;
|
|
||||||
constructor(private route: ActivatedRoute) {
|
/**
|
||||||
|
* The current page outlet string
|
||||||
|
*/
|
||||||
|
currentPage: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All possible page outlet strings
|
||||||
|
*/
|
||||||
|
pages: string[];
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute, private router: Router) {
|
||||||
|
this.router.events.subscribe(() => {
|
||||||
|
this.currentPage = this.route.snapshot.firstChild.routeConfig.path;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.pages = this.route.routeConfig.children
|
||||||
|
.map((child: any) => child.path)
|
||||||
|
.filter((path: string) => isNotEmpty(path)); // ignore reroutes
|
||||||
this.itemRD$ = this.route.data.pipe(map((data) => data.item));
|
this.itemRD$ = this.route.data.pipe(map((data) => data.item));
|
||||||
this.params$ = this.route.params;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
import {ItemPageResolver} from '../item-page.resolver';
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
import {NgModule} from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import {RouterModule} from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import {EditItemPageComponent} from './edit-item-page.component';
|
import { EditItemPageComponent } from './edit-item-page.component';
|
||||||
import {ItemWithdrawComponent} from './item-withdraw/item-withdraw.component';
|
import { ItemWithdrawComponent } from './item-withdraw/item-withdraw.component';
|
||||||
import {ItemReinstateComponent} from './item-reinstate/item-reinstate.component';
|
import { ItemReinstateComponent } from './item-reinstate/item-reinstate.component';
|
||||||
import {ItemPrivateComponent} from './item-private/item-private.component';
|
import { ItemPrivateComponent } from './item-private/item-private.component';
|
||||||
import {ItemPublicComponent} from './item-public/item-public.component';
|
import { ItemPublicComponent } from './item-public/item-public.component';
|
||||||
import {ItemDeleteComponent} from './item-delete/item-delete.component';
|
import { ItemDeleteComponent } from './item-delete/item-delete.component';
|
||||||
|
import { ItemStatusComponent } from './item-status/item-status.component';
|
||||||
|
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
||||||
|
|
||||||
const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
||||||
const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
||||||
@@ -25,7 +27,36 @@ const ITEM_EDIT_DELETE_PATH = 'delete';
|
|||||||
component: EditItemPageComponent,
|
component: EditItemPageComponent,
|
||||||
resolve: {
|
resolve: {
|
||||||
item: ItemPageResolver
|
item: ItemPageResolver
|
||||||
}
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'status'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'status',
|
||||||
|
component: ItemStatusComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'bitstreams',
|
||||||
|
/* TODO - change when bitstreams page exists */
|
||||||
|
component: ItemStatusComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'metadata',
|
||||||
|
component: ItemMetadataComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'view',
|
||||||
|
/* TODO - change when view page exists */
|
||||||
|
component: ItemStatusComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'curate',
|
||||||
|
/* TODO - change when curate page exists */
|
||||||
|
component: ItemStatusComponent
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ITEM_EDIT_WITHDRAW_PATH,
|
path: ITEM_EDIT_WITHDRAW_PATH,
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<!--{{metadata?.uuid}}-->
|
<!--{{metadata?.uuid}}-->
|
||||||
<td class="col-3">
|
<td class="col-3">
|
||||||
<div *ngIf="!(editable | async)">
|
<div *ngIf="!(editable | async)">
|
||||||
<span>{{metadata?.key}}</span>
|
<span>{{metadata?.key?.split('.').join('.​')}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="(editable | async)" class="field-container">
|
<div *ngIf="(editable | async)" class="field-container">
|
||||||
<ds-input-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
<ds-input-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
||||||
@@ -15,9 +15,12 @@
|
|||||||
(submitSuggestion)="update(suggestionControl.control)"
|
(submitSuggestion)="update(suggestionControl.control)"
|
||||||
(clickSuggestion)="update(suggestionControl.control)"
|
(clickSuggestion)="update(suggestionControl.control)"
|
||||||
(typeSuggestion)="update(suggestionControl.control)"
|
(typeSuggestion)="update(suggestionControl.control)"
|
||||||
|
(blur)="checkValidity(suggestionControl.control)"
|
||||||
(findSuggestions)="findMetadataFieldSuggestions($event)"
|
(findSuggestions)="findMetadataFieldSuggestions($event)"
|
||||||
#suggestionControl="ngModel"
|
#suggestionControl="ngModel"
|
||||||
[dsInListValidator]="metadataFields | async"
|
[dsInListValidator]="metadataFields | async"
|
||||||
|
[valid]="(valid | async)"
|
||||||
|
dsAutoFocus autoFocusSelector=".suggestion_input"
|
||||||
></ds-input-suggestions>
|
></ds-input-suggestions>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-danger"
|
<small class="text-danger"
|
||||||
@@ -43,17 +46,17 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="col-2 text-center">
|
<td class="col-2 text-center">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button [disabled]="!(canSetEditable() | async)" *ngIf="!(editable | async)" (click)="setEditable(true)" class="btn btn-light btn-sm">
|
<button [disabled]="!(canSetEditable() | async)" *ngIf="!(editable | async)" (click)="setEditable(true)" class="btn btn-primary btn-sm" title="{{'item.edit.metadata.edit.buttons.edit' | translate}}">
|
||||||
<i class="fas fa-edit fa-fw text-primary"></i>
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button [disabled]="!(canSetUneditable() | async)" *ngIf="(editable | async)" (click)="setEditable(false)" class="btn btn-light btn-sm">
|
<button [disabled]="!(canSetUneditable() | async)" *ngIf="(editable | async)" (click)="setEditable(false)" class="btn btn-success btn-sm" title="{{'item.edit.metadata.edit.buttons.unedit' | translate}}">
|
||||||
<i class="fas fa-check fa-fw text-success"></i>
|
<i class="fas fa-check fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button [disabled]="!(canRemove() | async)" (click)="remove()" class="btn btn-light btn-sm">
|
<button [disabled]="!(canRemove() | async)" (click)="remove()" class="btn btn-danger btn-sm" title="{{'item.edit.metadata.edit.buttons.remove' | translate}}">
|
||||||
<i class="fas fa-trash-alt fa-fw text-danger"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button [disabled]="!(canUndo() | async)" (click)="removeChangesFromField()" class="btn btn-light btn-sm">
|
<button [disabled]="!(canUndo() | async)" (click)="removeChangesFromField()" class="btn btn-warning btn-sm" title="{{'item.edit.metadata.edit.buttons.undo' | translate}}">
|
||||||
<i class="fas fa-undo-alt fa-fw text-warning"></i>
|
<i class="fas fa-undo-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@@ -44,7 +44,7 @@ const metadatum = Object.assign(new Metadatum(), {
|
|||||||
language: 'en'
|
language: 'en'
|
||||||
});
|
});
|
||||||
|
|
||||||
const route = 'http://test-url.com/test-url';
|
const url = 'http://test-url.com/test-url';
|
||||||
const fieldUpdate = {
|
const fieldUpdate = {
|
||||||
field: metadatum,
|
field: metadatum,
|
||||||
changeType: undefined
|
changeType: undefined
|
||||||
@@ -92,7 +92,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
de = fixture.debugElement.query(By.css('div.d-flex'));
|
de = fixture.debugElement.query(By.css('div.d-flex'));
|
||||||
el = de.nativeElement;
|
el = de.nativeElement;
|
||||||
|
|
||||||
comp.route = route;
|
comp.url = url;
|
||||||
comp.fieldUpdate = fieldUpdate;
|
comp.fieldUpdate = fieldUpdate;
|
||||||
comp.metadata = metadatum;
|
comp.metadata = metadatum;
|
||||||
|
|
||||||
@@ -104,8 +104,8 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
comp.update();
|
comp.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call saveChangeFieldUpdate on the objectUpdatesService with the correct route and metadata', () => {
|
it('it should call saveChangeFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
|
||||||
expect(objectUpdatesService.saveChangeFieldUpdate).toHaveBeenCalledWith(route, metadatum);
|
expect(objectUpdatesService.saveChangeFieldUpdate).toHaveBeenCalledWith(url, metadatum);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,8 +145,8 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
comp.setEditable(editable);
|
comp.setEditable(editable);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call setEditableFieldUpdate on the objectUpdatesService with the correct route and uuid and false', () => {
|
it('it should call setEditableFieldUpdate on the objectUpdatesService with the correct url and uuid and false', () => {
|
||||||
expect(objectUpdatesService.setEditableFieldUpdate).toHaveBeenCalledWith(route, metadatum.uuid, editable);
|
expect(objectUpdatesService.setEditableFieldUpdate).toHaveBeenCalledWith(url, metadatum.uuid, editable);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -203,8 +203,8 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
comp.remove();
|
comp.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct route and metadata', () => {
|
it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
|
||||||
expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(route, metadatum);
|
expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(url, metadatum);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -213,8 +213,8 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
comp.removeChangesFromField();
|
comp.removeChangesFromField();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call removeChangesFromField on the objectUpdatesService with the correct route and uuid', () => {
|
it('it should call removeChangesFromField on the objectUpdatesService with the correct url and uuid', () => {
|
||||||
expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(route, metadatum.uuid);
|
expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, metadatum.uuid);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -10,7 +10,6 @@ import { InputSuggestion } from '../../../../shared/input-suggestions/input-sugg
|
|||||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
|
import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import { inListValidator } from '../../../../shared/utils/validator.functions';
|
|
||||||
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
import { FormControl } from '@angular/forms';
|
import { FormControl } from '@angular/forms';
|
||||||
|
|
||||||
@@ -28,9 +27,9 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
*/
|
*/
|
||||||
@Input() fieldUpdate: FieldUpdate;
|
@Input() fieldUpdate: FieldUpdate;
|
||||||
/**
|
/**
|
||||||
* The current route of this page
|
* The current url of this page
|
||||||
*/
|
*/
|
||||||
@Input() route: string;
|
@Input() url: string;
|
||||||
/**
|
/**
|
||||||
* The metadatum of this field
|
* The metadatum of this field
|
||||||
*/
|
*/
|
||||||
@@ -65,8 +64,8 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
* Sets up an observable that keeps track of the current editable and valid state of this field
|
* Sets up an observable that keeps track of the current editable and valid state of this field
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.editable = this.objectUpdatesService.isEditable(this.route, this.metadata.uuid);
|
this.editable = this.objectUpdatesService.isEditable(this.url, this.metadata.uuid);
|
||||||
this.valid = this.objectUpdatesService.isValid(this.route, this.metadata.uuid);
|
this.valid = this.objectUpdatesService.isValid(this.url, this.metadata.uuid);
|
||||||
this.metadataFields = this.findMetadataFields()
|
this.metadataFields = this.findMetadataFields()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,32 +73,41 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
* Sends a new change update for this field to the object updates service
|
* Sends a new change update for this field to the object updates service
|
||||||
*/
|
*/
|
||||||
update(control?: FormControl) {
|
update(control?: FormControl) {
|
||||||
this.objectUpdatesService.saveChangeFieldUpdate(this.route, this.metadata);
|
this.objectUpdatesService.saveChangeFieldUpdate(this.url, this.metadata);
|
||||||
if (hasValue(control)) {
|
if (hasValue(control)) {
|
||||||
this.objectUpdatesService.setValidFieldUpdate(this.route, this.metadata.uuid, control.valid);
|
this.checkValidity(control);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to check the validity of a form control
|
||||||
|
* @param control The form control to check
|
||||||
|
*/
|
||||||
|
private checkValidity(control: FormControl) {
|
||||||
|
control.updateValueAndValidity();
|
||||||
|
this.objectUpdatesService.setValidFieldUpdate(this.url, this.metadata.uuid, control.valid);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a new editable state for this field to the service to change it
|
* Sends a new editable state for this field to the service to change it
|
||||||
* @param editable The new editable state for this field
|
* @param editable The new editable state for this field
|
||||||
*/
|
*/
|
||||||
setEditable(editable: boolean) {
|
setEditable(editable: boolean) {
|
||||||
this.objectUpdatesService.setEditableFieldUpdate(this.route, this.metadata.uuid, editable);
|
this.objectUpdatesService.setEditableFieldUpdate(this.url, this.metadata.uuid, editable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a new remove update for this field to the object updates service
|
* Sends a new remove update for this field to the object updates service
|
||||||
*/
|
*/
|
||||||
remove() {
|
remove() {
|
||||||
this.objectUpdatesService.saveRemoveFieldUpdate(this.route, this.metadata);
|
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, this.metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies the object updates service that the updates for the current field can be removed
|
* Notifies the object updates service that the updates for the current field can be removed
|
||||||
*/
|
*/
|
||||||
removeChangesFromField() {
|
removeChangesFromField() {
|
||||||
this.objectUpdatesService.removeSingleFieldUpdate(this.route, this.metadata.uuid);
|
this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.metadata.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,15 +178,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
* @return an observable that emits true when the user should be able to remove this field and false when they should not
|
* @return an observable that emits true when the user should be able to remove this field and false when they should not
|
||||||
*/
|
*/
|
||||||
canRemove(): Observable<boolean> {
|
canRemove(): Observable<boolean> {
|
||||||
return this.editable.pipe(
|
return observableOf(this.fieldUpdate.changeType !== FieldChangeType.REMOVE && this.fieldUpdate.changeType !== FieldChangeType.ADD);
|
||||||
map((editable: boolean) => {
|
|
||||||
if (editable) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return this.fieldUpdate.changeType !== FieldChangeType.REMOVE && this.fieldUpdate.changeType !== FieldChangeType.ADD;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,7 +186,9 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
* @return an observable that emits true when the user should be able to undo changes to this field and false when they should not
|
* @return an observable that emits true when the user should be able to undo changes to this field and false when they should not
|
||||||
*/
|
*/
|
||||||
canUndo(): Observable<boolean> {
|
canUndo(): Observable<boolean> {
|
||||||
return observableOf(this.fieldUpdate.changeType >= 0);
|
return this.editable.pipe(
|
||||||
|
map((editable: boolean) => this.fieldUpdate.changeType >= 0 || editable)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected isNotEmpty(value): boolean {
|
protected isNotEmpty(value): boolean {
|
||||||
|
@@ -5,8 +5,7 @@
|
|||||||
(click)="add()"><i
|
(click)="add()"><i
|
||||||
class="fas fa-plus"></i> {{"item.edit.metadata.add-button" | translate}}
|
class="fas fa-plus"></i> {{"item.edit.metadata.add-button" | translate}}
|
||||||
</button>
|
</button>
|
||||||
|
<div class="my-2 spaced-btn-group">
|
||||||
<div class="btn-group btn-group-toggle my-2" data-toggle="buttons">
|
|
||||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||||
[disabled]="!(hasChanges() | async)"
|
[disabled]="!(hasChanges() | async)"
|
||||||
(click)="discard()"><i
|
(click)="discard()"><i
|
||||||
@@ -32,24 +31,26 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate">
|
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate">
|
||||||
<ds-edit-in-place-field [fieldUpdate]="updateValue || {}"
|
<ds-edit-in-place-field [fieldUpdate]="updateValue || {}"
|
||||||
[route]="route"></ds-edit-in-place-field>
|
[url]="url"></ds-edit-in-place-field>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="btn-group btn-group-toggle my-2 float-right" data-toggle="buttons">
|
<div class="button-row">
|
||||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
<div class="my-2 float-right spaced-btn-group">
|
||||||
[disabled]="!(hasChanges() | async)"
|
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||||
(click)="discard()"><i
|
[disabled]="!(hasChanges() | async)"
|
||||||
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
|
(click)="discard()"><i
|
||||||
</button>
|
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
|
||||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
</button>
|
||||||
(click)="reinstate()"><i
|
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||||
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
|
(click)="reinstate()"><i
|
||||||
</button>
|
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
|
||||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
|
</button>
|
||||||
(click)="submit()"><i
|
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
|
||||||
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
(click)="submit()"><i
|
||||||
</button>
|
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,5 +1,13 @@
|
|||||||
@import '../../../../styles/variables.scss';
|
@import '../../../../styles/variables.scss';
|
||||||
|
|
||||||
.button-row .btn {
|
.button-row {
|
||||||
min-width: $button-min-width;
|
.spaced-btn-group > .btn {
|
||||||
|
margin-right: 0.5 * $spacer;
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
min-width: $button-min-width;
|
||||||
|
}
|
||||||
}
|
}
|
@@ -7,7 +7,7 @@ import { Metadatum } from '../../../core/shared/metadatum.model';
|
|||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import { SharedModule } from '../../../shared/shared.module';
|
import { SharedModule } from '../../../shared/shared.module';
|
||||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
import { Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
@@ -32,6 +32,8 @@ const infoNotification: INotification = new Notification('id', NotificationType.
|
|||||||
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
|
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const router = new RouterStub();
|
const router = new RouterStub();
|
||||||
|
let routeStub;
|
||||||
|
|
||||||
let itemService;
|
let itemService;
|
||||||
const notificationsService = jasmine.createSpyObj('notificationsService',
|
const notificationsService = jasmine.createSpyObj('notificationsService',
|
||||||
{
|
{
|
||||||
@@ -56,9 +58,9 @@ const metadatum3 = Object.assign(new Metadatum(), {
|
|||||||
value: 'Shakespeare, William',
|
value: 'Shakespeare, William',
|
||||||
});
|
});
|
||||||
|
|
||||||
const route = 'http://test-url.com/test-url';
|
const url = 'http://test-url.com/test-url';
|
||||||
|
|
||||||
router.url = route;
|
router.url = url;
|
||||||
|
|
||||||
const fieldUpdate1 = {
|
const fieldUpdate1 = {
|
||||||
field: metadatum1,
|
field: metadatum1,
|
||||||
@@ -84,6 +86,11 @@ describe('ItemMetadataComponent', () => {
|
|||||||
update: observableOf(new RemoteData(false, false, true, undefined, item)),
|
update: observableOf(new RemoteData(false, false, true, undefined, item)),
|
||||||
commitUpdates: {}
|
commitUpdates: {}
|
||||||
});
|
});
|
||||||
|
routeStub = {
|
||||||
|
parent: {
|
||||||
|
data: observableOf({ item: new RemoteData(false, false, true, null, item) })
|
||||||
|
}
|
||||||
|
};
|
||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
||||||
{
|
{
|
||||||
@@ -111,6 +118,9 @@ describe('ItemMetadataComponent', () => {
|
|||||||
{ provide: ItemDataService, useValue: itemService },
|
{ provide: ItemDataService, useValue: itemService },
|
||||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute, useValue: routeStub
|
||||||
|
},
|
||||||
{ provide: NotificationsService, useValue: notificationsService },
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
{ provide: GLOBAL_CONFIG, useValue: { notifications: { timeOut: 10 } } as any }
|
{ provide: GLOBAL_CONFIG, useValue: { notifications: { timeOut: 10 } } as any }
|
||||||
], schemas: [
|
], schemas: [
|
||||||
@@ -118,16 +128,14 @@ describe('ItemMetadataComponent', () => {
|
|||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(ItemMetadataComponent);
|
fixture = TestBed.createComponent(ItemMetadataComponent);
|
||||||
comp = fixture.componentInstance; // EditInPlaceFieldComponent test instance
|
comp = fixture.componentInstance; // EditInPlaceFieldComponent test instance
|
||||||
de = fixture.debugElement.query(By.css('div.d-flex'));
|
de = fixture.debugElement.query(By.css('div.d-flex'));
|
||||||
el = de.nativeElement;
|
el = de.nativeElement;
|
||||||
comp.item = item;
|
comp.url = url;
|
||||||
comp.route = route;
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -137,8 +145,8 @@ describe('ItemMetadataComponent', () => {
|
|||||||
comp.add(md);
|
comp.add(md);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call saveAddFieldUpdate on the objectUpdatesService with the correct route and metadata', () => {
|
it('it should call saveAddFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
|
||||||
expect(objectUpdatesService.saveAddFieldUpdate).toHaveBeenCalledWith(route, md);
|
expect(objectUpdatesService.saveAddFieldUpdate).toHaveBeenCalledWith(url, md);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -147,8 +155,8 @@ describe('ItemMetadataComponent', () => {
|
|||||||
comp.discard();
|
comp.discard();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call discardFieldUpdates on the objectUpdatesService with the correct route and notification', () => {
|
it('it should call discardFieldUpdates on the objectUpdatesService with the correct url and notification', () => {
|
||||||
expect(objectUpdatesService.discardFieldUpdates).toHaveBeenCalledWith(route, infoNotification);
|
expect(objectUpdatesService.discardFieldUpdates).toHaveBeenCalledWith(url, infoNotification);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,8 +165,8 @@ describe('ItemMetadataComponent', () => {
|
|||||||
comp.reinstate();
|
comp.reinstate();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct route', () => {
|
it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct url', () => {
|
||||||
expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(route);
|
expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(url);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -167,10 +175,10 @@ describe('ItemMetadataComponent', () => {
|
|||||||
comp.submit();
|
comp.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct route and metadata', () => {
|
it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct url and metadata', () => {
|
||||||
expect(objectUpdatesService.getUpdatedFields).toHaveBeenCalledWith(route, comp.item.metadata);
|
expect(objectUpdatesService.getUpdatedFields).toHaveBeenCalledWith(url, comp.item.metadata);
|
||||||
expect(itemService.update).toHaveBeenCalledWith(comp.item);
|
expect(itemService.update).toHaveBeenCalledWith(comp.item);
|
||||||
expect(objectUpdatesService.getFieldUpdates).toHaveBeenCalledWith(route, comp.item.metadata);
|
expect(objectUpdatesService.getFieldUpdates).toHaveBeenCalledWith(url, comp.item.metadata);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@ import { Component, Inject, Input, OnInit } from '@angular/core';
|
|||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
import { Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
@@ -31,15 +31,15 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* The item to display the edit page for
|
* The item to display the edit page for
|
||||||
*/
|
*/
|
||||||
@Input() item: Item;
|
item: Item;
|
||||||
/**
|
/**
|
||||||
* The current values and updates for all this item's metadata fields
|
* The current values and updates for all this item's metadata fields
|
||||||
*/
|
*/
|
||||||
updates$: Observable<FieldUpdates>;
|
updates$: Observable<FieldUpdates>;
|
||||||
/**
|
/**
|
||||||
* The current route of this page
|
* The current url of this page
|
||||||
*/
|
*/
|
||||||
route: string;
|
url: string;
|
||||||
/**
|
/**
|
||||||
* The time span for being able to undo discarding changes
|
* The time span for being able to undo discarding changes
|
||||||
*/
|
*/
|
||||||
@@ -51,16 +51,25 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
private route: ActivatedRoute
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.route.parent.data.pipe(map((data) => data.item))
|
||||||
|
.pipe(
|
||||||
|
first(),
|
||||||
|
map((data: RemoteData<Item>) => data.payload)
|
||||||
|
).subscribe((item: Item) => {
|
||||||
|
this.item = item;
|
||||||
|
});
|
||||||
|
|
||||||
this.discardTimeOut = this.EnvConfig.notifications.timeOut;
|
this.discardTimeOut = this.EnvConfig.notifications.timeOut;
|
||||||
this.route = this.router.url;
|
this.url = this.router.url;
|
||||||
if (this.route.indexOf('?') > 0) {
|
if (this.url.indexOf('?') > 0) {
|
||||||
this.route = this.route.substr(0, this.route.indexOf('?'));
|
this.url = this.url.substr(0, this.url.indexOf('?'));
|
||||||
}
|
}
|
||||||
this.hasChanges().pipe(first()).subscribe((hasChanges) => {
|
this.hasChanges().pipe(first()).subscribe((hasChanges) => {
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
@@ -69,7 +78,8 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
this.checkLastModified();
|
this.checkLastModified();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.route, this.item.metadata);
|
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadata);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,7 +87,8 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
* @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum
|
* @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum
|
||||||
*/
|
*/
|
||||||
add(metadata: Metadatum = new Metadatum()) {
|
add(metadata: Metadatum = new Metadatum()) {
|
||||||
this.objectUpdatesService.saveAddFieldUpdate(this.route, metadata);
|
this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,21 +99,21 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
const title = this.translateService.instant('item.edit.metadata.notifications.discarded.title');
|
const title = this.translateService.instant('item.edit.metadata.notifications.discarded.title');
|
||||||
const content = this.translateService.instant('item.edit.metadata.notifications.discarded.content');
|
const content = this.translateService.instant('item.edit.metadata.notifications.discarded.content');
|
||||||
const undoNotification = this.notificationsService.info(title, content, { timeOut: this.discardTimeOut });
|
const undoNotification = this.notificationsService.info(title, content, { timeOut: this.discardTimeOut });
|
||||||
this.objectUpdatesService.discardFieldUpdates(this.route, undoNotification);
|
this.objectUpdatesService.discardFieldUpdates(this.url, undoNotification);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request the object updates service to undo discarding all changes to this item
|
* Request the object updates service to undo discarding all changes to this item
|
||||||
*/
|
*/
|
||||||
reinstate() {
|
reinstate() {
|
||||||
this.objectUpdatesService.reinstateFieldUpdates(this.route);
|
this.objectUpdatesService.reinstateFieldUpdates(this.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends all initial values of this item to the object updates service
|
* Sends all initial values of this item to the object updates service
|
||||||
*/
|
*/
|
||||||
private initializeOriginalFields() {
|
private initializeOriginalFields() {
|
||||||
this.objectUpdatesService.initialize(this.route, this.item.metadata, this.item.lastModified);
|
this.objectUpdatesService.initialize(this.url, this.item.metadata, this.item.lastModified);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prevent unnecessary rerendering so fields don't lose focus **/
|
/* Prevent unnecessary rerendering so fields don't lose focus **/
|
||||||
@@ -117,42 +128,42 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
submit() {
|
submit() {
|
||||||
this.isValid().pipe(first()).subscribe((isValid) => {
|
this.isValid().pipe(first()).subscribe((isValid) => {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
const metadata$: Observable<Identifiable[]> = this.objectUpdatesService.getUpdatedFields(this.route, this.item.metadata) as Observable<Metadatum[]>;
|
const metadata$: Observable<Identifiable[]> = this.objectUpdatesService.getUpdatedFields(this.url, this.item.metadata) as Observable<Metadatum[]>;
|
||||||
metadata$.pipe(
|
metadata$.pipe(
|
||||||
first(),
|
first(),
|
||||||
switchMap((metadata: Metadatum[]) => {
|
switchMap((metadata: Metadatum[]) => {
|
||||||
const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata });
|
const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata });
|
||||||
return this.itemService.update(updatedItem);
|
return this.itemService.update(updatedItem);
|
||||||
}),
|
}),
|
||||||
tap(() => this.itemService.commitUpdates()),
|
tap(() => this.itemService.commitUpdates()),
|
||||||
getSucceededRemoteData()
|
getSucceededRemoteData()
|
||||||
).subscribe(
|
).subscribe(
|
||||||
(rd: RemoteData<Item>) => {
|
(rd: RemoteData<Item>) => {
|
||||||
this.item = rd.payload;
|
this.item = rd.payload;
|
||||||
this.initializeOriginalFields();
|
this.initializeOriginalFields();
|
||||||
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.route, this.item.metadata);
|
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadata);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const title = this.translateService.instant('item.edit.metadata.notifications.invalid.title');
|
const title = this.translateService.instant('item.edit.metadata.notifications.invalid.title');
|
||||||
const content = this.translateService.instant('item.edit.metadata.notifications.invalid.content');
|
const content = this.translateService.instant('item.edit.metadata.notifications.invalid.content');
|
||||||
this.notificationsService.error(title, content);
|
this.notificationsService.error(title, content);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether or not there are currently updates for this item
|
* Checks whether or not there are currently updates for this item
|
||||||
*/
|
*/
|
||||||
hasChanges(): Observable<boolean> {
|
hasChanges(): Observable<boolean> {
|
||||||
return this.objectUpdatesService.hasUpdates(this.route);
|
return this.objectUpdatesService.hasUpdates(this.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether or not the item is currently reinstatable
|
* Checks whether or not the item is currently reinstatable
|
||||||
*/
|
*/
|
||||||
isReinstatable(): Observable<boolean> {
|
isReinstatable(): Observable<boolean> {
|
||||||
return this.objectUpdatesService.isReinstatable(this.route);
|
return this.objectUpdatesService.isReinstatable(this.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,7 +172,7 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
private checkLastModified() {
|
private checkLastModified() {
|
||||||
const currentVersion = this.item.lastModified;
|
const currentVersion = this.item.lastModified;
|
||||||
this.objectUpdatesService.getLastModified(this.route).pipe(first()).subscribe(
|
this.objectUpdatesService.getLastModified(this.url).pipe(first()).subscribe(
|
||||||
(updateVersion: Date) => {
|
(updateVersion: Date) => {
|
||||||
if (updateVersion.getDate() !== currentVersion.getDate()) {
|
if (updateVersion.getDate() !== currentVersion.getDate()) {
|
||||||
const title = this.translateService.instant('item.edit.metadata.notifications.outdated.title');
|
const title = this.translateService.instant('item.edit.metadata.notifications.outdated.title');
|
||||||
@@ -173,7 +184,10 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current page is entirely valid
|
||||||
|
*/
|
||||||
private isValid() {
|
private isValid() {
|
||||||
return this.objectUpdatesService.isValidPage(this.route);
|
return this.objectUpdatesService.isValidPage(this.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,11 +6,13 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
|
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
|
||||||
import { HostWindowService } from '../../../shared/host-window.service';
|
import { HostWindowService } from '../../../shared/host-window.service';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { RouterStub } from '../../../shared/testing/router-stub';
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
|
||||||
describe('ItemStatusComponent', () => {
|
describe('ItemStatusComponent', () => {
|
||||||
let comp: ItemStatusComponent;
|
let comp: ItemStatusComponent;
|
||||||
@@ -27,12 +29,19 @@ describe('ItemStatusComponent', () => {
|
|||||||
url: `${itemPageUrl}/edit`
|
url: `${itemPageUrl}/edit`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const routeStub = {
|
||||||
|
parent: {
|
||||||
|
data: observableOf({ item: new RemoteData(false, false, true, null, mockItem) })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
declarations: [ItemStatusComponent],
|
declarations: [ItemStatusComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: Router, useValue: routerStub },
|
{ provide: Router, useValue: routerStub },
|
||||||
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
|
||||||
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -41,7 +50,6 @@ describe('ItemStatusComponent', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(ItemStatusComponent);
|
fixture = TestBed.createComponent(ItemStatusComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
comp.item = mockItem;
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,4 +73,5 @@ describe('ItemStatusComponent', () => {
|
|||||||
expect(statusItemPage.textContent).toContain(itemPageUrl);
|
expect(statusItemPage.textContent).toContain(itemPageUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
})
|
||||||
|
;
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
import {fadeIn, fadeInOut} from '../../../shared/animations/fade';
|
import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {Router} from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {ItemOperation} from '../item-operation/itemOperation.model';
|
import { ItemOperation } from '../item-operation/itemOperation.model';
|
||||||
|
import { first, map } from 'rxjs/operators';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-status',
|
selector: 'ds-item-status',
|
||||||
@@ -21,7 +24,7 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* The item to display the status for
|
* The item to display the status for
|
||||||
*/
|
*/
|
||||||
@Input() item: Item;
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data to show in the status
|
* The data to show in the status
|
||||||
@@ -37,39 +40,46 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
* key: id value: url to action's component
|
* key: id value: url to action's component
|
||||||
*/
|
*/
|
||||||
operations: ItemOperation[];
|
operations: ItemOperation[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The keys of the actions (to loop over)
|
* The keys of the actions (to loop over)
|
||||||
*/
|
*/
|
||||||
actionsKeys;
|
actionsKeys;
|
||||||
|
|
||||||
constructor(private router: Router) {
|
constructor(private router: Router, private route: ActivatedRoute) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.statusData = Object.assign({
|
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.item));
|
||||||
id: this.item.id,
|
this.itemRD$.pipe(
|
||||||
handle: this.item.handle,
|
first(),
|
||||||
lastModified: this.item.lastModified
|
map((data: RemoteData<Item>) => data.payload)
|
||||||
|
).subscribe((item: Item) => {
|
||||||
|
this.statusData = Object.assign({
|
||||||
|
id: item.id,
|
||||||
|
handle: item.handle,
|
||||||
|
lastModified: item.lastModified
|
||||||
|
});
|
||||||
|
this.statusDataKeys = Object.keys(this.statusData);
|
||||||
|
/*
|
||||||
|
The key is used to build messages
|
||||||
|
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
|
||||||
|
The value is supposed to be a href for the button
|
||||||
|
*/
|
||||||
|
this.operations = [];
|
||||||
|
if (item.isWithdrawn) {
|
||||||
|
this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl() + '/reinstate'));
|
||||||
|
} else {
|
||||||
|
this.operations.push(new ItemOperation('withdraw', this.getCurrentUrl() + '/withdraw'));
|
||||||
|
}
|
||||||
|
if (item.isDiscoverable) {
|
||||||
|
this.operations.push(new ItemOperation('private', this.getCurrentUrl() + '/private'));
|
||||||
|
} else {
|
||||||
|
this.operations.push(new ItemOperation('public', this.getCurrentUrl() + '/public'));
|
||||||
|
}
|
||||||
|
this.operations.push(new ItemOperation('delete', this.getCurrentUrl() + '/delete'));
|
||||||
});
|
});
|
||||||
this.statusDataKeys = Object.keys(this.statusData);
|
|
||||||
|
|
||||||
/*
|
|
||||||
The key is used to build messages
|
|
||||||
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
|
|
||||||
The value is supposed to be a href for the button
|
|
||||||
*/
|
|
||||||
this.operations = [];
|
|
||||||
if (this.item.isWithdrawn) {
|
|
||||||
this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl() + '/reinstate'));
|
|
||||||
} else {
|
|
||||||
this.operations.push(new ItemOperation('withdraw', this.getCurrentUrl() + '/withdraw'));
|
|
||||||
}
|
|
||||||
if (this.item.isDiscoverable) {
|
|
||||||
this.operations.push(new ItemOperation('private', this.getCurrentUrl() + '/private'));
|
|
||||||
} else {
|
|
||||||
this.operations.push(new ItemOperation('public', this.getCurrentUrl() + '/public'));
|
|
||||||
}
|
|
||||||
this.operations.push(new ItemOperation('delete', this.getCurrentUrl() + '/delete'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -15,7 +15,7 @@ export function getItemEditPath(id: string) {
|
|||||||
return new URLCombiner(getItemModulePath(),ITEM_EDIT_PATH.replace(/:id/, id)).toString()
|
return new URLCombiner(getItemModulePath(),ITEM_EDIT_PATH.replace(/:id/, id)).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const ITEM_EDIT_PATH = ':id/edit/:page';
|
const ITEM_EDIT_PATH = ':id/edit';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -39,7 +39,7 @@ const ITEM_EDIT_PATH = ':id/edit/:page';
|
|||||||
path: ITEM_EDIT_PATH,
|
path: ITEM_EDIT_PATH,
|
||||||
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
||||||
canActivate: [AuthenticatedGuard]
|
canActivate: [AuthenticatedGuard]
|
||||||
}
|
},
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@@ -22,10 +22,10 @@ export class MetadataField implements ListableObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
schema: MetadataSchema;
|
schema: MetadataSchema;
|
||||||
|
|
||||||
toString(): string {
|
toString(separator: string = '.'): string {
|
||||||
let key = this.schema.prefix + '.' + this.element;
|
let key = this.schema.prefix + separator + this.element;
|
||||||
if (isNotEmpty(this.qualifier)) {
|
if (isNotEmpty(this.qualifier)) {
|
||||||
key += '.' + this.qualifier;
|
key += separator + this.qualifier;
|
||||||
}
|
}
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
@@ -5,8 +5,10 @@
|
|||||||
(dsClickOutside)="close()">
|
(dsClickOutside)="close()">
|
||||||
<input #inputField type="text" [(ngModel)]="value" [name]="name"
|
<input #inputField type="text" [(ngModel)]="value" [name]="name"
|
||||||
class="form-control suggestion_input"
|
class="form-control suggestion_input"
|
||||||
|
[ngClass]="{'is-invalid': !valid}"
|
||||||
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
|
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
|
||||||
[placeholder]="placeholder"
|
[placeholder]="placeholder"
|
||||||
|
(blur)="blur.emit($event);"
|
||||||
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
|
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
|
||||||
<input type="submit" class="d-none"/>
|
<input type="submit" class="d-none"/>
|
||||||
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
||||||
|
@@ -60,6 +60,11 @@ export class InputSuggestionsComponent implements ControlValueAccessor, OnChange
|
|||||||
*/
|
*/
|
||||||
@Input() name;
|
@Input() name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the current input is valid
|
||||||
|
*/
|
||||||
|
@Input() valid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Output for when the form is submitted
|
* Output for when the form is submitted
|
||||||
*/
|
*/
|
||||||
@@ -80,6 +85,11 @@ export class InputSuggestionsComponent implements ControlValueAccessor, OnChange
|
|||||||
*/
|
*/
|
||||||
@Output() findSuggestions = new EventEmitter();
|
@Output() findSuggestions = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits event when the input field loses focus
|
||||||
|
*/
|
||||||
|
@Output() blur = new EventEmitter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits true when the list of suggestions should be shown
|
* Emits true when the list of suggestions should be shown
|
||||||
*/
|
*/
|
||||||
@@ -238,4 +248,8 @@ export class InputSuggestionsComponent implements ControlValueAccessor, OnChange
|
|||||||
this._value = val;
|
this._value = val;
|
||||||
this.propagateChange(this._value);
|
this.propagateChange(this._value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.queryInput.nativeElement.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -93,6 +93,7 @@ import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/del
|
|||||||
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
|
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
|
||||||
import { ObjectValuesPipe } from './utils/object-values-pipe';
|
import { ObjectValuesPipe } from './utils/object-values-pipe';
|
||||||
import { InListValidator } from './utils/in-list-validator.directive';
|
import { InListValidator } from './utils/in-list-validator.directive';
|
||||||
|
import { AutoFocusDirective } from './utils/auto-focus.directive';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -199,7 +200,8 @@ const DIRECTIVES = [
|
|||||||
DragClickDirective,
|
DragClickDirective,
|
||||||
DebounceDirective,
|
DebounceDirective,
|
||||||
ClickOutsideDirective,
|
ClickOutsideDirective,
|
||||||
InListValidator
|
InListValidator,
|
||||||
|
AutoFocusDirective
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
22
src/app/shared/utils/auto-focus.directive.ts
Normal file
22
src/app/shared/utils/auto-focus.directive.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Directive, AfterViewInit, ElementRef, Input } from '@angular/core';
|
||||||
|
import { isNotEmpty } from '../empty.util';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[dsAutoFocus]'
|
||||||
|
})
|
||||||
|
export class AutoFocusDirective implements AfterViewInit {
|
||||||
|
|
||||||
|
@Input() autoFocusSelector: string;
|
||||||
|
|
||||||
|
constructor(private el: ElementRef) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
if (isNotEmpty(this.autoFocusSelector)) {
|
||||||
|
return this.el.nativeElement.querySelector(this.autoFocusSelector).focus();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return this.el.nativeElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user