mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge pull request #335 from atmire/Move-item-component
Move item component
This commit is contained in:
@@ -238,6 +238,17 @@
|
|||||||
"item.edit.modify.overview.field": "Field",
|
"item.edit.modify.overview.field": "Field",
|
||||||
"item.edit.modify.overview.language": "Language",
|
"item.edit.modify.overview.language": "Language",
|
||||||
"item.edit.modify.overview.value": "Value",
|
"item.edit.modify.overview.value": "Value",
|
||||||
|
"item.edit.move.cancel": "Cancel",
|
||||||
|
"item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.",
|
||||||
|
"item.edit.move.error": "An error occured when attempting to move the item",
|
||||||
|
"item.edit.move.head": "Move item: {{id}}",
|
||||||
|
"item.edit.move.inheritpolicies.checkbox": "Inherit policies",
|
||||||
|
"item.edit.move.inheritpolicies.description": "Inherit the default policies of the destination collection",
|
||||||
|
"item.edit.move.move": "Move",
|
||||||
|
"item.edit.move.processing": "Moving...",
|
||||||
|
"item.edit.move.search.placeholder": "Enter a search query to look for collections",
|
||||||
|
"item.edit.move.success": "The item has been moved succesfully",
|
||||||
|
"item.edit.move.title": "Move item",
|
||||||
"item.edit.private.cancel": "Cancel",
|
"item.edit.private.cancel": "Cancel",
|
||||||
"item.edit.private.confirm": "Make it Private",
|
"item.edit.private.confirm": "Make it Private",
|
||||||
"item.edit.private.description": "Are you sure this item should be made private in the archive?",
|
"item.edit.private.description": "Are you sure this item should be made private in the archive?",
|
||||||
|
@@ -18,6 +18,7 @@ import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.compo
|
|||||||
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
||||||
import { EditRelationshipComponent } from './item-relationships/edit-relationship/edit-relationship.component';
|
import { EditRelationshipComponent } from './item-relationships/edit-relationship/edit-relationship.component';
|
||||||
import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
|
import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
|
||||||
|
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module that contains all components related to the Edit Item page administrator functionality
|
* Module that contains all components related to the Edit Item page administrator functionality
|
||||||
@@ -44,7 +45,8 @@ import { EditRelationshipListComponent } from './item-relationships/edit-relatio
|
|||||||
ItemBitstreamsComponent,
|
ItemBitstreamsComponent,
|
||||||
EditInPlaceFieldComponent,
|
EditInPlaceFieldComponent,
|
||||||
EditRelationshipComponent,
|
EditRelationshipComponent,
|
||||||
EditRelationshipListComponent
|
EditRelationshipListComponent,
|
||||||
|
ItemMoveComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class EditItemPageModule {
|
export class EditItemPageModule {
|
||||||
|
@@ -10,6 +10,7 @@ import { ItemDeleteComponent } from './item-delete/item-delete.component';
|
|||||||
import { ItemStatusComponent } from './item-status/item-status.component';
|
import { ItemStatusComponent } from './item-status/item-status.component';
|
||||||
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
||||||
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
|
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
|
||||||
|
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||||
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
||||||
|
|
||||||
const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
||||||
@@ -17,6 +18,7 @@ const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
|||||||
const ITEM_EDIT_PRIVATE_PATH = 'private';
|
const ITEM_EDIT_PRIVATE_PATH = 'private';
|
||||||
const ITEM_EDIT_PUBLIC_PATH = 'public';
|
const ITEM_EDIT_PUBLIC_PATH = 'public';
|
||||||
const ITEM_EDIT_DELETE_PATH = 'delete';
|
const ITEM_EDIT_DELETE_PATH = 'delete';
|
||||||
|
const ITEM_EDIT_MOVE_PATH = 'move';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routing module that handles the routing for the Edit Item page administrator functionality
|
* Routing module that handles the routing for the Edit Item page administrator functionality
|
||||||
@@ -104,6 +106,14 @@ const ITEM_EDIT_DELETE_PATH = 'delete';
|
|||||||
resolve: {
|
resolve: {
|
||||||
item: ItemPageResolver
|
item: ItemPageResolver
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ITEM_EDIT_MOVE_PATH,
|
||||||
|
component: ItemMoveComponent,
|
||||||
|
data: { title: 'item.edit.move.title' },
|
||||||
|
resolve: {
|
||||||
|
item: ItemPageResolver
|
||||||
|
}
|
||||||
}])
|
}])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<span>{{metadata?.key?.split('.').join('.​')}}</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-filter-input-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
||||||
[(ngModel)]="metadata.key"
|
[(ngModel)]="metadata.key"
|
||||||
(submitSuggestion)="update(suggestionControl)"
|
(submitSuggestion)="update(suggestionControl)"
|
||||||
(clickSuggestion)="update(suggestionControl)"
|
(clickSuggestion)="update(suggestionControl)"
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
[valid]="(valid | async) !== false"
|
[valid]="(valid | async) !== false"
|
||||||
dsAutoFocus autoFocusSelector=".suggestion_input"
|
dsAutoFocus autoFocusSelector=".suggestion_input"
|
||||||
[ngModelOptions]="{standalone: true}"
|
[ngModelOptions]="{standalone: true}"
|
||||||
></ds-input-suggestions>
|
></ds-filter-input-suggestions>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-danger"
|
<small class="text-danger"
|
||||||
*ngIf="(valid | async) === false">{{"item.edit.metadata.metadatafield.invalid" | translate}}</small>
|
*ngIf="(valid | async) === false">{{"item.edit.metadata.metadatafield.invalid" | translate}}</small>
|
||||||
|
@@ -10,7 +10,6 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { SharedModule } from '../../../../shared/shared.module';
|
import { SharedModule } from '../../../../shared/shared.module';
|
||||||
import { getTestScheduler } from 'jasmine-marbles';
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
@@ -18,6 +17,7 @@ import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
|
|||||||
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||||
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||||
|
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||||
|
|
||||||
let comp: EditInPlaceFieldComponent;
|
let comp: EditInPlaceFieldComponent;
|
||||||
let fixture: ComponentFixture<EditInPlaceFieldComponent>;
|
let fixture: ComponentFixture<EditInPlaceFieldComponent>;
|
||||||
|
@@ -4,13 +4,13 @@ import { RegistryService } from '../../../../core/registry/registry.service';
|
|||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||||
import { map, take } from 'rxjs/operators';
|
import { map, take } from 'rxjs/operators';
|
||||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
|
||||||
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 { NgModel } from '@angular/forms';
|
import { NgModel } from '@angular/forms';
|
||||||
import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
|
import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
|
||||||
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
|
||||||
|
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
// tslint:disable-next-line:component-selector
|
// tslint:disable-next-line:component-selector
|
||||||
|
@@ -0,0 +1,48 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2>{{'item.edit.move.head' | translate: {id: (itemRD$ | async)?.payload?.handle} }}</h2>
|
||||||
|
<p>{{'item.edit.move.description' | translate}}</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<ds-dso-input-suggestions #f id="search-form"
|
||||||
|
[suggestions]="(collectionSearchResults | async)"
|
||||||
|
[placeholder]="'item.edit.move.search.placeholder'| translate"
|
||||||
|
[action]="getCurrentUrl()"
|
||||||
|
[name]="'item-move'"
|
||||||
|
[(ngModel)]="selectedCollectionName"
|
||||||
|
(clickSuggestion)="onClick($event)"
|
||||||
|
(typeSuggestion)="resetCollection($event)"
|
||||||
|
(findSuggestions)="findSuggestions($event)"
|
||||||
|
(click)="f.open()"
|
||||||
|
ngDefaultControl>
|
||||||
|
</ds-dso-input-suggestions>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<p>
|
||||||
|
<input type="checkbox" name="tc" [(ngModel)]="inheritPolicies" id="inheritPoliciesCheckbox">
|
||||||
|
<label for="inheritPoliciesCheckbox">{{'item.edit.move.inheritpolicies.checkbox' |
|
||||||
|
translate}}</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{'item.edit.move.inheritpolicies.description' | translate}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button (click)="moveCollection()" class="btn btn-primary" [disabled]=!canSubmit>
|
||||||
|
<span *ngIf="!processing"> {{'item.edit.move.move' | translate}}</span>
|
||||||
|
<span *ngIf="processing"><i class='fas fa-circle-notch fa-spin'></i>
|
||||||
|
{{'item.edit.move.processing' | translate}}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button [routerLink]="['/items/', (itemRD$ | async)?.payload?.id, 'edit']"
|
||||||
|
class="btn btn-outline-secondary">
|
||||||
|
{{'item.edit.move.cancel' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,172 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ItemMoveComponent } from './item-move.component';
|
||||||
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { SearchService } from '../../../+search-page/search-service/search.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
|
||||||
|
describe('ItemMoveComponent', () => {
|
||||||
|
let comp: ItemMoveComponent;
|
||||||
|
let fixture: ComponentFixture<ItemMoveComponent>;
|
||||||
|
|
||||||
|
const mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018'
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemPageUrl = `fake-url/${mockItem.id}`;
|
||||||
|
const routerStub = Object.assign(new RouterStub(), {
|
||||||
|
url: `${itemPageUrl}/edit`
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItemDataService = jasmine.createSpyObj({
|
||||||
|
moveToCollection: observableOf(new RestResponse(true, 200, 'Success'))
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItemDataServiceFail = jasmine.createSpyObj({
|
||||||
|
moveToCollection: observableOf(new RestResponse(false, 500, 'Internal server error'))
|
||||||
|
});
|
||||||
|
|
||||||
|
const routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
item: new RemoteData(false, false, true, null, {
|
||||||
|
id: 'item1'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const collection1 = Object.assign(new Collection(),{
|
||||||
|
uuid: 'collection-uuid-1',
|
||||||
|
name: 'Test collection 1',
|
||||||
|
self: 'self-link-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const collection2 = Object.assign(new Collection(),{
|
||||||
|
uuid: 'collection-uuid-2',
|
||||||
|
name: 'Test collection 2',
|
||||||
|
self: 'self-link-2',
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockSearchService = {
|
||||||
|
search: () => {
|
||||||
|
return observableOf(new RemoteData(false, false, true, null,
|
||||||
|
new PaginatedList(null, [
|
||||||
|
{
|
||||||
|
indexableObject: collection1,
|
||||||
|
hitHighlights: {}
|
||||||
|
}, {
|
||||||
|
indexableObject: collection2,
|
||||||
|
hitHighlights: {}
|
||||||
|
}
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
describe('ItemMoveComponent success', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [ItemMoveComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
|
{provide: Router, useValue: routerStub},
|
||||||
|
{provide: ItemDataService, useValue: mockItemDataService},
|
||||||
|
{provide: NotificationsService, useValue: notificationsServiceStub},
|
||||||
|
{provide: SearchService, useValue: mockSearchService},
|
||||||
|
], schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemMoveComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should load suggestions', () => {
|
||||||
|
const expected = [
|
||||||
|
collection1,
|
||||||
|
collection2
|
||||||
|
];
|
||||||
|
|
||||||
|
comp.collectionSearchResults.subscribe((value) => {
|
||||||
|
expect(value).toEqual(expected);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should get current url ', () => {
|
||||||
|
expect(comp.getCurrentUrl()).toEqual('fake-url/fake-id/edit');
|
||||||
|
});
|
||||||
|
it('should on click select the correct collection name and id', () => {
|
||||||
|
const data = collection1;
|
||||||
|
|
||||||
|
comp.onClick(data);
|
||||||
|
|
||||||
|
expect(comp.selectedCollectionName).toEqual('Test collection 1');
|
||||||
|
expect(comp.selectedCollection).toEqual(collection1);
|
||||||
|
});
|
||||||
|
describe('moveCollection', () => {
|
||||||
|
it('should call itemDataService.moveToCollection', () => {
|
||||||
|
comp.itemId = 'item-id';
|
||||||
|
comp.selectedCollectionName = 'selected-collection-id';
|
||||||
|
comp.selectedCollection = collection1;
|
||||||
|
comp.moveCollection();
|
||||||
|
|
||||||
|
expect(mockItemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1);
|
||||||
|
});
|
||||||
|
it('should call notificationsService success message on success', () => {
|
||||||
|
comp.moveCollection();
|
||||||
|
|
||||||
|
expect(notificationsServiceStub.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ItemMoveComponent fail', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [ItemMoveComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
|
{provide: Router, useValue: routerStub},
|
||||||
|
{provide: ItemDataService, useValue: mockItemDataServiceFail},
|
||||||
|
{provide: NotificationsService, useValue: notificationsServiceStub},
|
||||||
|
{provide: SearchService, useValue: mockSearchService},
|
||||||
|
], schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemMoveComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call notificationsService error message on fail', () => {
|
||||||
|
comp.moveCollection();
|
||||||
|
|
||||||
|
expect(notificationsServiceStub.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,139 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { SearchService } from '../../../+search-page/search-service/search.service';
|
||||||
|
import { first, map } from 'rxjs/operators';
|
||||||
|
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
|
||||||
|
import { SearchOptions } from '../../../+search-page/search-options.model';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import { SearchResult } from '../../../+search-page/search-result.model';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { getItemEditPath } from '../../item-page-routing.module';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
import { tap } from 'rxjs/internal/operators/tap';
|
||||||
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-move',
|
||||||
|
templateUrl: './item-move.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component that handles the moving of an item to a different collection
|
||||||
|
*/
|
||||||
|
export class ItemMoveComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* TODO: There is currently no backend support to change the owningCollection and inherit policies,
|
||||||
|
* TODO: when this is added, the inherit policies option should be used.
|
||||||
|
*/
|
||||||
|
|
||||||
|
selectorType = DSpaceObjectType.COLLECTION;
|
||||||
|
|
||||||
|
inheritPolicies = false;
|
||||||
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
|
collectionSearchResults: Observable<any[]> = observableOf([]);
|
||||||
|
selectedCollectionName: string;
|
||||||
|
selectedCollection: Collection;
|
||||||
|
canSubmit = false;
|
||||||
|
|
||||||
|
itemId: string;
|
||||||
|
processing = false;
|
||||||
|
|
||||||
|
pagination = new PaginationComponentOptions();
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private itemDataService: ItemDataService,
|
||||||
|
private searchService: SearchService,
|
||||||
|
private translateService: TranslateService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.itemRD$ = this.route.data.pipe(map((data) => data.item), getSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
||||||
|
this.itemRD$.subscribe((rd) => {
|
||||||
|
this.itemId = rd.payload.id;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.pagination.pageSize = 5;
|
||||||
|
this.loadSuggestions('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find suggestions based on entered query
|
||||||
|
* @param query - Search query
|
||||||
|
*/
|
||||||
|
findSuggestions(query): void {
|
||||||
|
this.loadSuggestions(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all available collections to move the item to.
|
||||||
|
* TODO: When the API support it, only fetch collections where user has ADD rights to.
|
||||||
|
*/
|
||||||
|
loadSuggestions(query): void {
|
||||||
|
this.collectionSearchResults = this.searchService.search(new PaginatedSearchOptions({
|
||||||
|
pagination: this.pagination,
|
||||||
|
dsoType: DSpaceObjectType.COLLECTION,
|
||||||
|
query: query
|
||||||
|
})).pipe(
|
||||||
|
first(),
|
||||||
|
map((rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) => {
|
||||||
|
return rd.payload.page.map((searchResult) => {
|
||||||
|
return searchResult.indexableObject
|
||||||
|
})
|
||||||
|
}) ,
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the collection name and id based on the selected value
|
||||||
|
* @param data - obtained from the ds-input-suggestions component
|
||||||
|
*/
|
||||||
|
onClick(data: any): void {
|
||||||
|
this.selectedCollection = data;
|
||||||
|
this.selectedCollectionName = data.name;
|
||||||
|
this.canSubmit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string} the current URL
|
||||||
|
*/
|
||||||
|
getCurrentUrl() {
|
||||||
|
return this.router.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the item to a new collection based on the selected collection
|
||||||
|
*/
|
||||||
|
moveCollection() {
|
||||||
|
this.processing = true;
|
||||||
|
this.itemDataService.moveToCollection(this.itemId, this.selectedCollection).pipe(first()).subscribe(
|
||||||
|
(response: RestResponse) => {
|
||||||
|
this.router.navigate([getItemEditPath(this.itemId)]);
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
this.notificationsService.success(this.translateService.get('item.edit.move.success'));
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get('item.edit.move.error'));
|
||||||
|
}
|
||||||
|
this.processing = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the can submit when the user changes the content of the input field
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
resetCollection(data: any) {
|
||||||
|
this.canSubmit = false;
|
||||||
|
}
|
||||||
|
}
|
@@ -4,7 +4,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!operation.disabled" class="col-9 float-left action-button">
|
<div *ngIf="!operation.disabled" class="col-9 float-left action-button">
|
||||||
<a class="btn btn-outline-secondary" href="{{operation.operationUrl}}">
|
<a class="btn btn-outline-secondary" [routerLink]="operation.operationUrl">
|
||||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,6 +3,7 @@ import {async, TestBed} from '@angular/core/testing';
|
|||||||
import { ItemOperationComponent } from './item-operation.component';
|
import { ItemOperationComponent } from './item-operation.component';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
describe('ItemOperationComponent', () => {
|
describe('ItemOperationComponent', () => {
|
||||||
let itemOperation: ItemOperation;
|
let itemOperation: ItemOperation;
|
||||||
@@ -12,7 +13,7 @@ describe('ItemOperationComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot()],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
declarations: [ItemOperationComponent]
|
declarations: [ItemOperationComponent]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
@@ -79,6 +79,7 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
this.operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public'));
|
this.operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public'));
|
||||||
}
|
}
|
||||||
this.operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete'));
|
this.operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete'));
|
||||||
|
this.operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move'));
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
| translate}}</a>
|
| translate}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-input-suggestions [suggestions]="(filterSearchResults | async)"
|
<ds-filter-input-suggestions [suggestions]="(filterSearchResults | async)"
|
||||||
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate"
|
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate"
|
||||||
[action]="currentUrl"
|
[action]="currentUrl"
|
||||||
[name]="filterConfig.paramName"
|
[name]="filterConfig.paramName"
|
||||||
@@ -23,5 +23,5 @@
|
|||||||
(submitSuggestion)="onSubmit($event)"
|
(submitSuggestion)="onSubmit($event)"
|
||||||
(clickSuggestion)="onSubmit($event)"
|
(clickSuggestion)="onSubmit($event)"
|
||||||
(findSuggestions)="findSuggestions($event)"
|
(findSuggestions)="findSuggestions($event)"
|
||||||
ngDefaultControl></ds-input-suggestions>
|
ngDefaultControl></ds-filter-input-suggestions>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -21,9 +21,9 @@ import { SearchService } from '../../../search-service/search.service';
|
|||||||
import { FILTER_CONFIG, IN_PLACE_SEARCH, SearchFilterService } from '../search-filter.service';
|
import { FILTER_CONFIG, IN_PLACE_SEARCH, SearchFilterService } from '../search-filter.service';
|
||||||
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
||||||
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
|
||||||
import { SearchOptions } from '../../../search-options.model';
|
import { SearchOptions } from '../../../search-options.model';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-facet-filter',
|
selector: 'ds-search-facet-filter',
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
| translate}}</a>
|
| translate}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-input-suggestions [suggestions]="(filterSearchResults | async)"
|
<ds-filter-input-suggestions [suggestions]="(filterSearchResults | async)"
|
||||||
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate"
|
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate"
|
||||||
[action]="currentUrl"
|
[action]="currentUrl"
|
||||||
[name]="filterConfig.paramName"
|
[name]="filterConfig.paramName"
|
||||||
@@ -24,5 +24,5 @@
|
|||||||
(clickSuggestion)="onClick($event)"
|
(clickSuggestion)="onClick($event)"
|
||||||
(findSuggestions)="findSuggestions($event)"
|
(findSuggestions)="findSuggestions($event)"
|
||||||
ngDefaultControl
|
ngDefaultControl
|
||||||
></ds-input-suggestions>
|
></ds-filter-input-suggestions>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
| translate}}</a>
|
| translate}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-input-suggestions [suggestions]="(filterSearchResults | async)"
|
<ds-filter-input-suggestions [suggestions]="(filterSearchResults | async)"
|
||||||
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate"
|
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate"
|
||||||
[action]="currentUrl"
|
[action]="currentUrl"
|
||||||
[name]="filterConfig.paramName"
|
[name]="filterConfig.paramName"
|
||||||
@@ -23,5 +23,5 @@
|
|||||||
(submitSuggestion)="onSubmit($event)"
|
(submitSuggestion)="onSubmit($event)"
|
||||||
(clickSuggestion)="onClick($event)"
|
(clickSuggestion)="onClick($event)"
|
||||||
(findSuggestions)="findSuggestions($event)"
|
(findSuggestions)="findSuggestions($event)"
|
||||||
ngDefaultControl></ds-input-suggestions>
|
ngDefaultControl></ds-filter-input-suggestions>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, find, map } from 'rxjs/operators';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { BrowseService } from '../browse/browse.service';
|
import { BrowseService } from '../browse/browse.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
@@ -12,14 +12,17 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
|||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { FindAllOptions, PatchRequest, RestRequest } from './request.models';
|
import { FindAllOptions, PatchRequest, PutRequest, RestRequest } from './request.models';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { configureRequest, getRequestFromRequestHref } from '../shared/operators';
|
import { configureRequest, getRequestFromRequestHref } from '../shared/operators';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
|
import { Collection } from '../shared/collection.model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemDataService extends DataService<Item> {
|
export class ItemDataService extends DataService<Item> {
|
||||||
@@ -118,4 +121,43 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
map((requestEntry: RequestEntry) => requestEntry.response)
|
map((requestEntry: RequestEntry) => requestEntry.response)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint to move the item
|
||||||
|
* @param itemId
|
||||||
|
*/
|
||||||
|
public getMoveItemEndpoint(itemId: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
map((endpoint: string) => this.getIDHref(endpoint, itemId)),
|
||||||
|
map((endpoint: string) => `${endpoint}/owningCollection`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the item to a different owning collection
|
||||||
|
* @param itemId
|
||||||
|
* @param collection
|
||||||
|
*/
|
||||||
|
public moveToCollection(itemId: string, collection: Collection): Observable<RestResponse> {
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
|
options.headers = headers;
|
||||||
|
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
const hrefObs = this.getMoveItemEndpoint(itemId);
|
||||||
|
|
||||||
|
hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
map((href: string) => {
|
||||||
|
const request = new PutRequest(requestId, href, collection.self, options);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
find((request: RequestEntry) => request.completed),
|
||||||
|
map((request: RequestEntry) => request.response)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,25 @@
|
|||||||
|
<form #form="ngForm" (ngSubmit)="onSubmit(currentObject)"
|
||||||
|
[action]="action" (keydown)="onKeydown($event)"
|
||||||
|
(keydown.arrowdown)="shiftFocusDown($event)"
|
||||||
|
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
|
||||||
|
(dsClickOutside)="close();">
|
||||||
|
<input #inputField type="text" [(ngModel)]="value" [name]="name"
|
||||||
|
class="form-control suggestion_input mb-2"
|
||||||
|
[ngClass]="{'is-invalid': !valid}"
|
||||||
|
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
|
||||||
|
[placeholder]="placeholder"
|
||||||
|
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
|
||||||
|
<input type="submit" class="d-none"/>
|
||||||
|
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
||||||
|
<div class="dropdown-list">
|
||||||
|
<div *ngFor="let suggestionOption of suggestions">
|
||||||
|
<button class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption)" #suggestion>
|
||||||
|
<div class="click-blocker">
|
||||||
|
</div>
|
||||||
|
<ds-wrapper-list-element [object]="suggestionOption"></ds-wrapper-list-element>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
@@ -0,0 +1,71 @@
|
|||||||
|
import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { DsoInputSuggestionsComponent } from './dso-input-suggestions.component';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
describe('DsoInputSuggestionsComponent', () => {
|
||||||
|
|
||||||
|
let comp: DsoInputSuggestionsComponent;
|
||||||
|
let fixture: ComponentFixture<DsoInputSuggestionsComponent>;
|
||||||
|
let de: DebugElement;
|
||||||
|
let el: HTMLElement;
|
||||||
|
|
||||||
|
const dso1 = {
|
||||||
|
uuid: 'test-uuid-1',
|
||||||
|
name: 'test-name-1'
|
||||||
|
} as DSpaceObject;
|
||||||
|
|
||||||
|
const dso2 = {
|
||||||
|
uuid: 'test-uuid-2',
|
||||||
|
name: 'test-name-2'
|
||||||
|
} as DSpaceObject;
|
||||||
|
|
||||||
|
const dso3 = {
|
||||||
|
uuid: 'test-uuid-3',
|
||||||
|
name: 'test-name-3'
|
||||||
|
} as DSpaceObject;
|
||||||
|
|
||||||
|
const suggestions = [dso1, dso2, dso3];
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule],
|
||||||
|
declarations: [DsoInputSuggestionsComponent],
|
||||||
|
providers: [],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(DsoInputSuggestionsComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DsoInputSuggestionsComponent);
|
||||||
|
|
||||||
|
comp = fixture.componentInstance; // LoadingComponent test instance
|
||||||
|
comp.suggestions = suggestions;
|
||||||
|
// query for the message <label> by CSS element selector
|
||||||
|
de = fixture.debugElement;
|
||||||
|
el = de.nativeElement;
|
||||||
|
comp.show.next(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when an element is clicked', () => {
|
||||||
|
const clickedIndex = 0;
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp, 'onClickSuggestion');
|
||||||
|
const clickedLink = de.query(By.css('.dropdown-list > div:nth-child(' + (clickedIndex + 1) + ') button'));
|
||||||
|
clickedLink.triggerEventHandler('click', {});
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should call onClickSuggestion() with the suggestion as a parameter', () => {
|
||||||
|
expect(comp.onClickSuggestion).toHaveBeenCalledWith(suggestions[clickedIndex]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,47 @@
|
|||||||
|
import { Component, forwardRef, Input } from '@angular/core';
|
||||||
|
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
import { InputSuggestionsComponent } from '../input-suggestions.component';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-dso-input-suggestions',
|
||||||
|
styleUrls: ['./../input-suggestions.component.scss'],
|
||||||
|
templateUrl: './dso-input-suggestions.component.html',
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
// Usage of forwardRef necessary https://github.com/angular/angular.io/issues/1151
|
||||||
|
// tslint:disable-next-line:no-forward-ref
|
||||||
|
useExisting: forwardRef(() => DsoInputSuggestionsComponent),
|
||||||
|
multi: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component representing a form with a autocomplete functionality for DSpaceObjects
|
||||||
|
*/
|
||||||
|
export class DsoInputSuggestionsComponent extends InputSuggestionsComponent {
|
||||||
|
/**
|
||||||
|
* The suggestions that should be shown
|
||||||
|
*/
|
||||||
|
@Input() suggestions: DSpaceObject[] = [];
|
||||||
|
|
||||||
|
currentObject: DSpaceObject;
|
||||||
|
|
||||||
|
onSubmit(data: DSpaceObject) {
|
||||||
|
this.value = data.name;
|
||||||
|
this.currentObject = data;
|
||||||
|
this.submitSuggestion.emit(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickSuggestion(data: DSpaceObject) {
|
||||||
|
this.value = data.name;
|
||||||
|
this.currentObject = data;
|
||||||
|
this.clickSuggestion.emit(data);
|
||||||
|
this.close();
|
||||||
|
this.blockReopen = true;
|
||||||
|
this.queryInput.nativeElement.focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
<form #form="ngForm" (ngSubmit)="onSubmit(value)"
|
||||||
|
[action]="action" (keydown)="onKeydown($event)"
|
||||||
|
(keydown.arrowdown)="shiftFocusDown($event)"
|
||||||
|
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
|
||||||
|
(dsClickOutside)="close();">
|
||||||
|
<input #inputField type="text" [(ngModel)]="value" [name]="name"
|
||||||
|
class="form-control suggestion_input"
|
||||||
|
[ngClass]="{'is-invalid': !valid}"
|
||||||
|
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
|
||||||
|
[placeholder]="placeholder"
|
||||||
|
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
|
||||||
|
<input type="submit" class="d-none"/>
|
||||||
|
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
||||||
|
<div class="dropdown-list">
|
||||||
|
<div *ngFor="let suggestionOption of suggestions">
|
||||||
|
<a href="#" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption.value)" #suggestion>
|
||||||
|
<span [innerHTML]="suggestionOption.displayValue"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
@@ -0,0 +1,57 @@
|
|||||||
|
import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { FilterInputSuggestionsComponent } from './filter-input-suggestions.component';
|
||||||
|
|
||||||
|
describe('FilterInputSuggestionsComponent', () => {
|
||||||
|
|
||||||
|
let comp: FilterInputSuggestionsComponent;
|
||||||
|
let fixture: ComponentFixture<FilterInputSuggestionsComponent>;
|
||||||
|
let de: DebugElement;
|
||||||
|
let el: HTMLElement;
|
||||||
|
const suggestions = [{displayValue: 'suggestion uno', value: 'suggestion uno'}, {
|
||||||
|
displayValue: 'suggestion dos',
|
||||||
|
value: 'suggestion dos'
|
||||||
|
}, {displayValue: 'suggestion tres', value: 'suggestion tres'}];
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule],
|
||||||
|
declarations: [FilterInputSuggestionsComponent],
|
||||||
|
providers: [],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(FilterInputSuggestionsComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FilterInputSuggestionsComponent);
|
||||||
|
|
||||||
|
comp = fixture.componentInstance; // LoadingComponent test instance
|
||||||
|
comp.suggestions = suggestions;
|
||||||
|
// query for the message <label> by CSS element selector
|
||||||
|
de = fixture.debugElement;
|
||||||
|
el = de.nativeElement;
|
||||||
|
comp.show.next(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when an element is clicked', () => {
|
||||||
|
const clickedIndex = 0;
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp, 'onClickSuggestion');
|
||||||
|
const clickedLink = de.query(By.css('.dropdown-list > div:nth-child(' + (clickedIndex + 1) + ') a'));
|
||||||
|
clickedLink.triggerEventHandler('click', {});
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should call onClickSuggestion() with the suggestion as a parameter', () => {
|
||||||
|
expect(comp.onClickSuggestion).toHaveBeenCalledWith(suggestions[clickedIndex].value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,44 @@
|
|||||||
|
import { Component, forwardRef, Input } from '@angular/core';
|
||||||
|
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
import { InputSuggestionsComponent } from '../input-suggestions.component';
|
||||||
|
import { InputSuggestion } from '../input-suggestions.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-filter-input-suggestions',
|
||||||
|
styleUrls: ['./../input-suggestions.component.scss'],
|
||||||
|
templateUrl: './filter-input-suggestions.component.html',
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
// Usage of forwardRef necessary https://github.com/angular/angular.io/issues/1151
|
||||||
|
// tslint:disable-next-line:no-forward-ref
|
||||||
|
useExisting: forwardRef(() => FilterInputSuggestionsComponent),
|
||||||
|
multi: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component representing a form with a autocomplete functionality
|
||||||
|
*/
|
||||||
|
export class FilterInputSuggestionsComponent extends InputSuggestionsComponent {
|
||||||
|
/**
|
||||||
|
* The suggestions that should be shown
|
||||||
|
*/
|
||||||
|
@Input() suggestions: InputSuggestion[] = [];
|
||||||
|
|
||||||
|
onSubmit(data) {
|
||||||
|
this.value = data;
|
||||||
|
this.submitSuggestion.emit(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickSuggestion(data) {
|
||||||
|
this.value = data;
|
||||||
|
this.clickSuggestion.emit(data);
|
||||||
|
this.close();
|
||||||
|
this.blockReopen = true;
|
||||||
|
this.queryInput.nativeElement.focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,12 +1,23 @@
|
|||||||
.autocomplete {
|
.autocomplete {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
padding: $input-padding-y $input-padding-x;
|
padding: $input-padding-y $input-padding-x;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.click-blocker {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,22 +13,11 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import { hasValue, isNotEmpty } from '../empty.util';
|
import { hasValue, isNotEmpty } from '../empty.util';
|
||||||
import { InputSuggestion } from './input-suggestions.model';
|
|
||||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-input-suggestions',
|
selector: 'ds-input-suggestions',
|
||||||
styleUrls: ['./input-suggestions.component.scss'],
|
|
||||||
templateUrl: './input-suggestions.component.html',
|
templateUrl: './input-suggestions.component.html',
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: NG_VALUE_ACCESSOR,
|
|
||||||
// Usage of forwardRef necessary https://github.com/angular/angular.io/issues/1151
|
|
||||||
// tslint:disable-next-line:no-forward-ref
|
|
||||||
useExisting: forwardRef(() => InputSuggestionsComponent),
|
|
||||||
multi: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,7 +27,7 @@ export class InputSuggestionsComponent implements ControlValueAccessor, OnChange
|
|||||||
/**
|
/**
|
||||||
* The suggestions that should be shown
|
* The suggestions that should be shown
|
||||||
*/
|
*/
|
||||||
@Input() suggestions: InputSuggestion[] = [];
|
@Input() suggestions: any[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The time waited to detect if any other input will follow before requesting the suggestions
|
* The time waited to detect if any other input will follow before requesting the suggestions
|
||||||
@@ -188,6 +177,15 @@ export class InputSuggestionsComponent implements ControlValueAccessor, OnChange
|
|||||||
this.show.next(false);
|
this.show.next(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the show variable so the suggestion dropdown opens
|
||||||
|
*/
|
||||||
|
open() {
|
||||||
|
if (!this.blockReopen) {
|
||||||
|
this.show.next(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For usage of the isNotEmpty function in the template
|
* For usage of the isNotEmpty function in the template
|
||||||
*/
|
*/
|
||||||
@@ -195,16 +193,15 @@ export class InputSuggestionsComponent implements ControlValueAccessor, OnChange
|
|||||||
return isNotEmpty(data);
|
return isNotEmpty(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSubmit(data: any) {
|
||||||
|
// sub class should decide how to handle the date
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make sure that if a suggestion is clicked, the suggestions dropdown closes, does not reopen and the focus moves to the input field
|
* Make sure that if a suggestion is clicked, the suggestions dropdown closes, does not reopen and the focus moves to the input field
|
||||||
*/
|
*/
|
||||||
onClickSuggestion(data) {
|
onClickSuggestion(data: any) {
|
||||||
this.value = data;
|
// sub class should decide how to handle the date
|
||||||
this.clickSuggestion.emit(data);
|
|
||||||
this.close();
|
|
||||||
this.blockReopen = true;
|
|
||||||
this.queryInput.nativeElement.focus();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -219,11 +216,6 @@ export class InputSuggestionsComponent implements ControlValueAccessor, OnChange
|
|||||||
this.blockReopen = false;
|
this.blockReopen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(data) {
|
|
||||||
this.value = data;
|
|
||||||
this.submitSuggestion.emit(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* START - Method's needed to add ngModel (ControlValueAccessor) to a component */
|
/* START - Method's needed to add ngModel (ControlValueAccessor) to a component */
|
||||||
registerOnChange(fn: any): void {
|
registerOnChange(fn: any): void {
|
||||||
this.propagateChange = fn;
|
this.propagateChange = fn;
|
||||||
|
@@ -138,6 +138,8 @@ import { RoleDirective } from './roles/role.directive';
|
|||||||
import { UserMenuComponent } from './auth-nav-menu/user-menu/user-menu.component';
|
import { UserMenuComponent } from './auth-nav-menu/user-menu/user-menu.component';
|
||||||
import { ClaimedTaskActionsReturnToPoolComponent } from './mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component';
|
import { ClaimedTaskActionsReturnToPoolComponent } from './mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component';
|
||||||
import { ItemDetailPreviewFieldComponent } from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component';
|
import { ItemDetailPreviewFieldComponent } from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component';
|
||||||
|
import { FilterInputSuggestionsComponent } from './input-suggestions/filter-suggestions/filter-input-suggestions.component';
|
||||||
|
import { DsoInputSuggestionsComponent } from './input-suggestions/dso-input-suggestions/dso-input-suggestions.component';
|
||||||
import { TypedItemSearchResultGridElementComponent } from './object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component';
|
import { TypedItemSearchResultGridElementComponent } from './object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component';
|
||||||
import { PublicationGridElementComponent } from './object-grid/item-grid-element/item-types/publication/publication-grid-element.component';
|
import { PublicationGridElementComponent } from './object-grid/item-grid-element/item-types/publication/publication-grid-element.component';
|
||||||
import { ItemTypeBadgeComponent } from './object-list/item-type-badge/item-type-badge.component';
|
import { ItemTypeBadgeComponent } from './object-list/item-type-badge/item-type-badge.component';
|
||||||
@@ -162,7 +164,7 @@ const MODULES = [
|
|||||||
NouisliderModule,
|
NouisliderModule,
|
||||||
MomentModule,
|
MomentModule,
|
||||||
TextMaskModule,
|
TextMaskModule,
|
||||||
MenuModule
|
MenuModule,
|
||||||
];
|
];
|
||||||
|
|
||||||
const ROOT_MODULES = [
|
const ROOT_MODULES = [
|
||||||
@@ -248,6 +250,8 @@ const COMPONENTS = [
|
|||||||
TruncatablePartComponent,
|
TruncatablePartComponent,
|
||||||
BrowseByComponent,
|
BrowseByComponent,
|
||||||
InputSuggestionsComponent,
|
InputSuggestionsComponent,
|
||||||
|
FilterInputSuggestionsComponent,
|
||||||
|
DsoInputSuggestionsComponent,
|
||||||
DSOSelectorComponent,
|
DSOSelectorComponent,
|
||||||
CreateCommunityParentSelectorComponent,
|
CreateCommunityParentSelectorComponent,
|
||||||
CreateCollectionParentSelectorComponent,
|
CreateCollectionParentSelectorComponent,
|
||||||
|
Reference in New Issue
Block a user