diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 795b3cc747..12f4547cf6 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -188,7 +188,7 @@ "confirm": "Withdraw", "cancel": "Cancel", "success": "The item was withdrawn successfully", - "error": "An error occured while withdrawing the item" + "error": "An error occurred while withdrawing the item" }, "reinstate": { "header": "Reinstate item: {{ id }}", @@ -196,7 +196,7 @@ "confirm": "Reinstate", "cancel": "Cancel", "success": "The item was reinstated successfully", - "error": "An error occured while reinstating the item" + "error": "An error occurred while reinstating the item" }, "private": { "header": "Make item private: {{ id }}", @@ -204,7 +204,7 @@ "confirm": "Make it Private", "cancel": "Cancel", "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": { "header": "Make item public: {{ id }}", @@ -212,7 +212,7 @@ "confirm": "Make it Public", "cancel": "Cancel", "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": { "header": "Delete item: {{ id }}", @@ -220,7 +220,7 @@ "confirm": "Delete", "cancel": "Cancel", "success": "The item has been deleted", - "error": "An error occured while deleting the item" + "error": "An error occurred while deleting the item" }, "metadata": { "add-button": "Add", @@ -233,6 +233,14 @@ "language": "Lang", "edit": "Edit" }, + "edit": { + "buttons": { + "edit": "Edit", + "unedit": "Stop editing", + "remove": "Remove", + "undo": "Undo changes" + } + }, "metadatafield": { "invalid": "Please choose a valid metadata field" }, @@ -247,7 +255,7 @@ }, "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." } } } diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.html b/src/app/+item-page/edit-item-page/edit-item-page.component.html index 2cb05ab21f..f580d21d18 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.component.html +++ b/src/app/+item-page/edit-item-page/edit-item-page.component.html @@ -1,37 +1,21 @@
-
-
-

{{'item.edit.head' | translate}}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+

{{'item.edit.head' | translate}}

+ +
-
diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.ts b/src/app/+item-page/edit-item-page/edit-item-page.component.ts index a795dd3dee..d7ab3ea199 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.component.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.component.ts @@ -1,10 +1,11 @@ -import {fadeIn, fadeInOut} from '../../shared/animations/fade'; -import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core'; -import { ActivatedRoute, Params } from '@angular/router'; -import {RemoteData} from '../../core/data/remote-data'; -import {Item} from '../../core/shared/item.model'; -import {Observable} from 'rxjs'; -import {map} from 'rxjs/operators'; +import { fadeIn, fadeInOut } from '../../shared/animations/fade'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Params, Router } from '@angular/router'; +import { RemoteData } from '../../core/data/remote-data'; +import { Item } from '../../core/shared/item.model'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { isNotEmpty } from '../../shared/empty.util'; @Component({ selector: 'ds-edit-item-page', @@ -24,13 +25,27 @@ export class EditItemPageComponent implements OnInit { * The item to edit */ itemRD$: Observable>; - params$: Observable; - 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 { + 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.params$ = this.route.params; } - } diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index 8ef6f43e17..8a45c42ff6 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -1,12 +1,14 @@ -import {ItemPageResolver} from '../item-page.resolver'; -import {NgModule} from '@angular/core'; -import {RouterModule} from '@angular/router'; -import {EditItemPageComponent} from './edit-item-page.component'; -import {ItemWithdrawComponent} from './item-withdraw/item-withdraw.component'; -import {ItemReinstateComponent} from './item-reinstate/item-reinstate.component'; -import {ItemPrivateComponent} from './item-private/item-private.component'; -import {ItemPublicComponent} from './item-public/item-public.component'; -import {ItemDeleteComponent} from './item-delete/item-delete.component'; +import { ItemPageResolver } from '../item-page.resolver'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { EditItemPageComponent } from './edit-item-page.component'; +import { ItemWithdrawComponent } from './item-withdraw/item-withdraw.component'; +import { ItemReinstateComponent } from './item-reinstate/item-reinstate.component'; +import { ItemPrivateComponent } from './item-private/item-private.component'; +import { ItemPublicComponent } from './item-public/item-public.component'; +import { ItemDeleteComponent } from './item-delete/item-delete.component'; +import { ItemStatusComponent } from './item-status/item-status.component'; +import { ItemMetadataComponent } from './item-metadata/item-metadata.component'; const ITEM_EDIT_WITHDRAW_PATH = 'withdraw'; const ITEM_EDIT_REINSTATE_PATH = 'reinstate'; @@ -25,7 +27,36 @@ const ITEM_EDIT_DELETE_PATH = 'delete'; component: EditItemPageComponent, resolve: { 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, diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html index e4fdc60a1e..25f586d0ed 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html @@ -7,7 +7,7 @@
- {{metadata?.key}} + {{metadata?.key?.split('.').join('.​')}}
- - - -
diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts index 927a4a96b1..cefbb3620a 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts @@ -44,7 +44,7 @@ const metadatum = Object.assign(new Metadatum(), { language: 'en' }); -const route = 'http://test-url.com/test-url'; +const url = 'http://test-url.com/test-url'; const fieldUpdate = { field: metadatum, changeType: undefined @@ -92,7 +92,7 @@ describe('EditInPlaceFieldComponent', () => { de = fixture.debugElement.query(By.css('div.d-flex')); el = de.nativeElement; - comp.route = route; + comp.url = url; comp.fieldUpdate = fieldUpdate; comp.metadata = metadatum; @@ -104,8 +104,8 @@ describe('EditInPlaceFieldComponent', () => { comp.update(); }); - it('it should call saveChangeFieldUpdate on the objectUpdatesService with the correct route and metadata', () => { - expect(objectUpdatesService.saveChangeFieldUpdate).toHaveBeenCalledWith(route, metadatum); + it('it should call saveChangeFieldUpdate on the objectUpdatesService with the correct url and metadata', () => { + expect(objectUpdatesService.saveChangeFieldUpdate).toHaveBeenCalledWith(url, metadatum); }); }); @@ -145,8 +145,8 @@ describe('EditInPlaceFieldComponent', () => { 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); + it('it should call setEditableFieldUpdate on the objectUpdatesService with the correct url and uuid and false', () => { + expect(objectUpdatesService.setEditableFieldUpdate).toHaveBeenCalledWith(url, metadatum.uuid, editable); }); }); @@ -203,8 +203,8 @@ describe('EditInPlaceFieldComponent', () => { comp.remove(); }); - it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct route and metadata', () => { - expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(route, metadatum); + it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct url and metadata', () => { + expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(url, metadatum); }); }); @@ -213,8 +213,8 @@ describe('EditInPlaceFieldComponent', () => { comp.removeChangesFromField(); }); - it('it should call removeChangesFromField on the objectUpdatesService with the correct route and uuid', () => { - expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(route, metadatum.uuid); + it('it should call removeChangesFromField on the objectUpdatesService with the correct url and uuid', () => { + expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, metadatum.uuid); }); }); diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts index d0b35b7a82..f24de359b8 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts @@ -10,7 +10,6 @@ import { InputSuggestion } from '../../../../shared/input-suggestions/input-sugg import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { inListValidator } from '../../../../shared/utils/validator.functions'; import { getSucceededRemoteData } from '../../../../core/shared/operators'; import { FormControl } from '@angular/forms'; @@ -28,9 +27,9 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges { */ @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 */ @@ -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 */ ngOnInit(): void { - this.editable = this.objectUpdatesService.isEditable(this.route, this.metadata.uuid); - this.valid = this.objectUpdatesService.isValid(this.route, this.metadata.uuid); + this.editable = this.objectUpdatesService.isEditable(this.url, this.metadata.uuid); + this.valid = this.objectUpdatesService.isValid(this.url, this.metadata.uuid); 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 */ update(control?: FormControl) { - this.objectUpdatesService.saveChangeFieldUpdate(this.route, this.metadata); + this.objectUpdatesService.saveChangeFieldUpdate(this.url, this.metadata); 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 * @param editable The new editable state for this field */ 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 */ 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 */ 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 */ canRemove(): Observable { - return this.editable.pipe( - map((editable: boolean) => { - if (editable) { - return false; - } else { - return this.fieldUpdate.changeType !== FieldChangeType.REMOVE && this.fieldUpdate.changeType !== FieldChangeType.ADD; - } - }) - ); + return observableOf(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 */ canUndo(): Observable { - return observableOf(this.fieldUpdate.changeType >= 0); + return this.editable.pipe( + map((editable: boolean) => this.fieldUpdate.changeType >= 0 || editable) + ); } protected isNotEmpty(value): boolean { diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html index 36d18372bd..04896ee4a6 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html @@ -5,8 +5,7 @@ (click)="add()"> {{"item.edit.metadata.add-button" | translate}} - -
+
- - +
+
+ + + +
diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.scss b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.scss index b2994dcec7..1ae2839606 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.scss +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.scss @@ -1,5 +1,13 @@ @import '../../../../styles/variables.scss'; -.button-row .btn { - min-width: $button-min-width; +.button-row { + .spaced-btn-group > .btn { + margin-right: 0.5 * $spacer; + &:last-child { + margin-right: 0; + } + } + .btn { + min-width: $button-min-width; + } } \ No newline at end of file diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts index e57c4af67c..a9c8c4327f 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts @@ -7,7 +7,7 @@ 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 { ActivatedRoute, Router } from '@angular/router'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateModule } from '@ngx-translate/core'; 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 date = new Date(); const router = new RouterStub(); +let routeStub; + let itemService; const notificationsService = jasmine.createSpyObj('notificationsService', { @@ -56,9 +58,9 @@ const metadatum3 = Object.assign(new Metadatum(), { 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 = { field: metadatum1, @@ -84,6 +86,11 @@ describe('ItemMetadataComponent', () => { update: observableOf(new RemoteData(false, false, true, undefined, item)), commitUpdates: {} }); + routeStub = { + parent: { + data: observableOf({ item: new RemoteData(false, false, true, null, item) }) + } + }; scheduler = getTestScheduler(); objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', { @@ -111,6 +118,9 @@ describe('ItemMetadataComponent', () => { { provide: ItemDataService, useValue: itemService }, { provide: ObjectUpdatesService, useValue: objectUpdatesService }, { provide: Router, useValue: router }, + { + provide: ActivatedRoute, useValue: routeStub + }, { provide: NotificationsService, useValue: notificationsService }, { provide: GLOBAL_CONFIG, useValue: { notifications: { timeOut: 10 } } as any } ], schemas: [ @@ -118,16 +128,14 @@ describe('ItemMetadataComponent', () => { ] }).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; + comp.url = url; fixture.detectChanges(); }); @@ -137,8 +145,8 @@ describe('ItemMetadataComponent', () => { comp.add(md); }); - it('it should call saveAddFieldUpdate on the objectUpdatesService with the correct route and metadata', () => { - expect(objectUpdatesService.saveAddFieldUpdate).toHaveBeenCalledWith(route, md); + it('it should call saveAddFieldUpdate on the objectUpdatesService with the correct url and metadata', () => { + expect(objectUpdatesService.saveAddFieldUpdate).toHaveBeenCalledWith(url, md); }); }); @@ -147,8 +155,8 @@ describe('ItemMetadataComponent', () => { comp.discard(); }); - it('it should call discardFieldUpdates on the objectUpdatesService with the correct route and notification', () => { - expect(objectUpdatesService.discardFieldUpdates).toHaveBeenCalledWith(route, infoNotification); + it('it should call discardFieldUpdates on the objectUpdatesService with the correct url and notification', () => { + expect(objectUpdatesService.discardFieldUpdates).toHaveBeenCalledWith(url, infoNotification); }); }); @@ -157,8 +165,8 @@ describe('ItemMetadataComponent', () => { comp.reinstate(); }); - it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct route', () => { - expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(route); + it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct url', () => { + expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(url); }); }); @@ -167,10 +175,10 @@ describe('ItemMetadataComponent', () => { comp.submit(); }); - it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct route and metadata', () => { - expect(objectUpdatesService.getUpdatedFields).toHaveBeenCalledWith(route, comp.item.metadata); + it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct url and metadata', () => { + expect(objectUpdatesService.getUpdatedFields).toHaveBeenCalledWith(url, comp.item.metadata); expect(itemService.update).toHaveBeenCalledWith(comp.item); - expect(objectUpdatesService.getFieldUpdates).toHaveBeenCalledWith(route, comp.item.metadata); + expect(objectUpdatesService.getFieldUpdates).toHaveBeenCalledWith(url, comp.item.metadata); }); }); diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts index ae32028486..380e05c334 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts @@ -2,7 +2,7 @@ import { Component, Inject, Input, OnInit } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; import { ItemDataService } from '../../../core/data/item-data.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { cloneDeep } from 'lodash'; import { Observable } from 'rxjs'; import { @@ -31,15 +31,15 @@ export class ItemMetadataComponent implements OnInit { /** * 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 */ updates$: Observable; /** - * 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 */ @@ -51,16 +51,25 @@ export class ItemMetadataComponent implements OnInit { private router: Router, private notificationsService: NotificationsService, private translateService: TranslateService, - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + private route: ActivatedRoute ) { } ngOnInit(): void { + this.route.parent.data.pipe(map((data) => data.item)) + .pipe( + first(), + map((data: RemoteData) => data.payload) + ).subscribe((item: Item) => { + this.item = item; + }); + 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.url = this.router.url; + if (this.url.indexOf('?') > 0) { + this.url = this.url.substr(0, this.url.indexOf('?')); } this.hasChanges().pipe(first()).subscribe((hasChanges) => { if (!hasChanges) { @@ -69,7 +78,8 @@ export class ItemMetadataComponent implements OnInit { 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 */ 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 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); + this.objectUpdatesService.discardFieldUpdates(this.url, undoNotification); } /** * Request the object updates service to undo discarding all changes to this item */ reinstate() { - this.objectUpdatesService.reinstateFieldUpdates(this.route); + this.objectUpdatesService.reinstateFieldUpdates(this.url); } /** * 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); + this.objectUpdatesService.initialize(this.url, this.item.metadata, this.item.lastModified); } /* Prevent unnecessary rerendering so fields don't lose focus **/ @@ -117,42 +128,42 @@ export class ItemMetadataComponent implements OnInit { submit() { this.isValid().pipe(first()).subscribe((isValid) => { if (isValid) { - const metadata$: Observable = this.objectUpdatesService.getUpdatedFields(this.route, this.item.metadata) as Observable; - metadata$.pipe( - first(), - switchMap((metadata: Metadatum[]) => { - const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata }); - return this.itemService.update(updatedItem); - }), - tap(() => this.itemService.commitUpdates()), - getSucceededRemoteData() - ).subscribe( - (rd: RemoteData) => { - this.item = rd.payload; - this.initializeOriginalFields(); - this.updates$ = this.objectUpdatesService.getFieldUpdates(this.route, this.item.metadata); - } - ) - } else { - const title = this.translateService.instant('item.edit.metadata.notifications.invalid.title'); - const content = this.translateService.instant('item.edit.metadata.notifications.invalid.content'); - this.notificationsService.error(title, content); - } - }); + const metadata$: Observable = this.objectUpdatesService.getUpdatedFields(this.url, this.item.metadata) as Observable; + metadata$.pipe( + first(), + switchMap((metadata: Metadatum[]) => { + const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata }); + return this.itemService.update(updatedItem); + }), + tap(() => this.itemService.commitUpdates()), + getSucceededRemoteData() + ).subscribe( + (rd: RemoteData) => { + this.item = rd.payload; + this.initializeOriginalFields(); + this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadata); + } + ) + } else { + const title = this.translateService.instant('item.edit.metadata.notifications.invalid.title'); + const content = this.translateService.instant('item.edit.metadata.notifications.invalid.content'); + this.notificationsService.error(title, content); + } + }); } /** * Checks whether or not there are currently updates for this item */ hasChanges(): Observable { - return this.objectUpdatesService.hasUpdates(this.route); + return this.objectUpdatesService.hasUpdates(this.url); } /** * Checks whether or not the item is currently reinstatable */ isReinstatable(): Observable { - return this.objectUpdatesService.isReinstatable(this.route); + return this.objectUpdatesService.isReinstatable(this.url); } /** @@ -161,7 +172,7 @@ export class ItemMetadataComponent implements OnInit { */ private checkLastModified() { const currentVersion = this.item.lastModified; - this.objectUpdatesService.getLastModified(this.route).pipe(first()).subscribe( + this.objectUpdatesService.getLastModified(this.url).pipe(first()).subscribe( (updateVersion: Date) => { if (updateVersion.getDate() !== currentVersion.getDate()) { 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() { - return this.objectUpdatesService.isValidPage(this.route); + return this.objectUpdatesService.isValidPage(this.url); } } diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts index 319d4c47ae..9c3049c638 100644 --- a/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts @@ -6,11 +6,13 @@ import { CommonModule } from '@angular/common'; import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub'; import { HostWindowService } from '../../../shared/host-window.service'; 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 { Item } from '../../../core/shared/item.model'; 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', () => { let comp: ItemStatusComponent; @@ -27,12 +29,19 @@ describe('ItemStatusComponent', () => { url: `${itemPageUrl}/edit` }); + const routeStub = { + parent: { + data: observableOf({ item: new RemoteData(false, false, true, null, mockItem) }) + } + }; + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], declarations: [ItemStatusComponent], providers: [ { provide: Router, useValue: routerStub }, + { provide: ActivatedRoute, useValue: routeStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); @@ -41,7 +50,6 @@ describe('ItemStatusComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(ItemStatusComponent); comp = fixture.componentInstance; - comp.item = mockItem; fixture.detectChanges(); }); @@ -65,4 +73,5 @@ describe('ItemStatusComponent', () => { expect(statusItemPage.textContent).toContain(itemPageUrl); }); -}); +}) +; diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts index 2b2c7a2ed4..28cd23a5fe 100644 --- a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts @@ -1,8 +1,11 @@ -import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; -import {fadeIn, fadeInOut} from '../../../shared/animations/fade'; -import {Item} from '../../../core/shared/item.model'; -import {Router} from '@angular/router'; -import {ItemOperation} from '../item-operation/itemOperation.model'; +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; +import { Item } from '../../../core/shared/item.model'; +import { ActivatedRoute, Router } from '@angular/router'; +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({ selector: 'ds-item-status', @@ -21,7 +24,7 @@ export class ItemStatusComponent implements OnInit { /** * The item to display the status for */ - @Input() item: Item; + itemRD$: Observable>; /** * The data to show in the status @@ -37,39 +40,46 @@ export class ItemStatusComponent implements OnInit { * key: id value: url to action's component */ operations: ItemOperation[]; + /** * The keys of the actions (to loop over) */ actionsKeys; - constructor(private router: Router) { + constructor(private router: Router, private route: ActivatedRoute) { } ngOnInit(): void { - this.statusData = Object.assign({ - id: this.item.id, - handle: this.item.handle, - lastModified: this.item.lastModified + this.itemRD$ = this.route.parent.data.pipe(map((data) => data.item)); + this.itemRD$.pipe( + first(), + map((data: RemoteData) => 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..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..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')); } /** diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts index 962f659076..ec562842aa 100644 --- a/src/app/+item-page/item-page-routing.module.ts +++ b/src/app/+item-page/item-page-routing.module.ts @@ -15,7 +15,7 @@ export function getItemEditPath(id: string) { 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({ imports: [ @@ -39,7 +39,7 @@ const ITEM_EDIT_PATH = ':id/edit/:page'; path: ITEM_EDIT_PATH, loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule', canActivate: [AuthenticatedGuard] - } + }, ]) ], providers: [ diff --git a/src/app/core/metadata/metadatafield.model.ts b/src/app/core/metadata/metadatafield.model.ts index 36f97d8d6f..ba28b59d0e 100644 --- a/src/app/core/metadata/metadatafield.model.ts +++ b/src/app/core/metadata/metadatafield.model.ts @@ -22,10 +22,10 @@ export class MetadataField implements ListableObject { @autoserialize schema: MetadataSchema; - toString(): string { - let key = this.schema.prefix + '.' + this.element; + toString(separator: string = '.'): string { + let key = this.schema.prefix + separator + this.element; if (isNotEmpty(this.qualifier)) { - key += '.' + this.qualifier; + key += separator + this.qualifier; } return key; } diff --git a/src/app/shared/input-suggestions/input-suggestions.component.html b/src/app/shared/input-suggestions/input-suggestions.component.html index b620f4b79a..1bd3cde22e 100644 --- a/src/app/shared/input-suggestions/input-suggestions.component.html +++ b/src/app/shared/input-suggestions/input-suggestions.component.html @@ -5,8 +5,10 @@ (dsClickOutside)="close()">