5590: Item move component

This commit is contained in:
Yana De Pauw
2018-10-25 18:14:22 +02:00
parent 8570ff2578
commit d26bba8e14
15 changed files with 351 additions and 140 deletions

View File

@@ -103,6 +103,16 @@
"curate": {
"head": "Curate"
}
},
"move": {
"head":"Move item: {{id}}",
"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.",
"inheritpolicies": {
"description": "Inherit the default policies of the destination collection",
"checkbox": "Inherit policies"
},
"move": "Move",
"cancel": "Cancel"
}
}
},

View File

@@ -4,15 +4,21 @@ import { SharedModule } from '../../shared/shared.module';
import { EditItemPageRoutingModule } from './edit-item-page.routing.module';
import { EditItemPageComponent } from './edit-item-page.component';
import { ItemStatusComponent } from './item-status/item-status.component';
import {ItemOperationComponent} from './item-operation/item-operation.component';
import {ItemMoveComponent} from './item-move/item-move.component';
import {SearchPageModule} from '../../+search-page/search-page.module';
@NgModule({
imports: [
CommonModule,
SharedModule,
EditItemPageRoutingModule
EditItemPageRoutingModule,
SearchPageModule.forRoot(),
],
declarations: [
EditItemPageComponent,
ItemOperationComponent,
ItemMoveComponent,
ItemStatusComponent
]
})

View File

@@ -2,6 +2,15 @@ import { ItemPageResolver } from '../item-page.resolver';
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {EditItemPageComponent} from './edit-item-page.component';
import {ItemMoveComponent} from './item-move/item-move.component';
import {URLCombiner} from '../../core/url-combiner/url-combiner';
import {getItemEditPath} from '../item-page-routing.module';
const ITEM_EDIT_MOVE_PATH = 'move';
export function getItemEditMovePath(id: string) {
return new URLCombiner(getItemEditPath(id), ITEM_EDIT_MOVE_PATH);
}
@NgModule({
imports: [
@@ -12,6 +21,13 @@ import { EditItemPageComponent } from './edit-item-page.component';
resolve: {
item: ItemPageResolver
}
},
{
path: ITEM_EDIT_MOVE_PATH,
component: ItemMoveComponent,
resolve: {
item: ItemPageResolver
}
}
])
],

View File

@@ -1,51 +1,41 @@
import {Component, OnInit} from '@angular/core';
import {Collection} from '../../../core/shared/collection.model';
import {RemoteData} from '../../../core/data/remote-data';
import {Item} from '../../../core/shared/item.model';
import {getSucceededRemoteData} from '../../../core/shared/operators';
import {Observable} from 'rxjs';
import {PaginatedList} from '../../../core/data/paginated-list';
import {TranslateService} from '@ngx-translate/core';
import {NotificationsService} from '../../../shared/notifications/notifications.service';
import {SearchService} from '../../../+search-page/search-service/search.service';
import {SearchConfigurationService} from '../../../+search-page/search-service/search-configuration.service';
import {ActivatedRoute, Router} from '@angular/router';
import {CollectionDataService} from '../../../core/data/collection-data.service';
<div class="container">
<div class="row">
<div class="col-12">
<h2>{{'item.edit.move.head' | translate: { id: (itemRD$ | async)?.payload?.id} }}</h2>
<p>{{'item.edit.move.description' | translate}}</p>
<div class="row">
<div class="col-12">
<ds-input-suggestions #f id="search-form"
[suggestions]="(filterSearchResults | async)"
[placeholder]="'item.move.search.placeholder'| translate"
[action]="getCurrentUrl()"
[name]="'item-move'"
[(ngModel)]="selectedCollection"
(clickSuggestion)="onClick($event)"
(findSuggestions)="findSuggestions($event)"
(click)="f.open()"
ngDefaultControl>
</ds-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>
@Component({
selector: 'ds-item-move',
templateUrl: './item-move.component.html'
})
export class ItemMoveComponent implements OnInit {
inheritPolicies: boolean;
itemRD$: Observable<RemoteData<Item>>;
/**
* List of collections to show under the "Browse" tab
* Collections that are mapped to the item
*/
itemCollectionsRD$: Observable<RemoteData<PaginatedList<Collection>>>;
constructor(private route: ActivatedRoute,
private router: Router,
private searchConfigService: SearchConfigurationService,
private searchService: SearchService,
private notificationsService: NotificationsService,
private collectionDataService: CollectionDataService,
private translateService: TranslateService) {
}
ngOnInit(): void {
this.itemRD$ = this.route.data.map((data) => data.item).pipe(getSucceededRemoteData()) as Observable<RemoteData<Item>>;
this.loadCollectionLists();
}
/**
* Load all available collections to move the item to.
* TODO: When the API support it, only fetch collections where user has ADD rights to.
*/
loadCollectionLists() {
this.itemCollectionsRD$ = this.collectionDataService.findAll();
}
}
<button (click)="moveCollection()" class="btn btn-outline-secondary">{{'item.edit.move.move' | translate}}
</button>
<button [routerLink]="['/items/', (itemRD$ | async)?.payload?.id]" class="btn btn-outline-secondary">
{{'item.edit.move.cancel' | translate}}
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,111 @@
import {Component, OnInit} from '@angular/core';
import {SearchService} from '../../../+search-page/search-service/search.service';
import {Observable} from 'rxjs/Observable';
import {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 {PaginatedSearchOptions} from '../../../+search-page/paginated-search-options.model';
import {Item} from '../../../core/shared/item.model';
import {ActivatedRoute, Router} from '@angular/router';
import {NotificationsService} from '../../../shared/notifications/notifications.service';
import {CollectionDataService} from '../../../core/data/collection-data.service';
import {SearchConfigurationService} from '../../../+search-page/search-service/search-configuration.service';
import {TranslateService} from '@ngx-translate/core';
import {getSucceededRemoteData} from '../../../core/shared/operators';
import {ItemDataService} from '../../../core/data/item-data.service';
import {RestResponse} from '../../../core/cache/response-cache.models';
import {getItemEditPath} from '../../item-page-routing.module';
@Component({
selector: 'ds-item-move',
templateUrl: './item-move.component.html'
})
export class ItemMoveComponent implements OnInit {
inheritPolicies = false;
itemRD$: Observable<RemoteData<Item>>;
/**
* Search options
*/
searchOptions$: Observable<PaginatedSearchOptions>;
filterSearchResults: Observable<any[]> = Observable.of([]);
selectedCollection: string;
selectedCollectionId: string;
itemId: string;
constructor(private route: ActivatedRoute,
private router: Router,
private notificationsService: NotificationsService,
private collectionDataService: CollectionDataService,
private itemDataService: ItemDataService,
private searchConfigService: SearchConfigurationService,
private searchService: SearchService,
private translateService: TranslateService) {
}
ngOnInit(): void {
this.itemRD$ = this.route.data.map((data) => data.item).pipe(getSucceededRemoteData()) as Observable<RemoteData<Item>>;
this.itemRD$.first().subscribe((rd) => {
this.itemId = rd.payload.id;
}
);
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
this.loadSuggestions('');
}
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.filterSearchResults = this.searchService.search(new SearchOptions({
dsoType: DSpaceObjectType.COLLECTION,
query: query
})).first().pipe(
map((rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) => {
return rd.payload.page.map((searchResult) => {
return {
displayValue: searchResult.dspaceObject.name,
value: {name: searchResult.dspaceObject.name, id: searchResult.dspaceObject.uuid}
};
});
})
);
}
onClick(data: any): void {
this.selectedCollection = data.name;
this.selectedCollectionId = data.id;
}
/**
* @returns {string} the current URL
*/
getCurrentUrl() {
return this.router.url;
}
moveCollection() {
this.itemDataService.moveToCollection(this.itemId, this.selectedCollectionId).first().subscribe(
(response: RestResponse) => {
this.router.navigate([getItemEditPath(this.itemId)]);
if (response.isSuccessful) {
this.notificationsService.success(this.translateService.get('item.move.success'));
} else {
this.notificationsService.error(this.translateService.get('item.move.error'));
}
}
);
}
}

View File

@@ -1,10 +1,15 @@
import {Component} from '@angular/core';
@Component({
selector: 'ds-item-operation',
templateUrl: './ds-item-operation.html'
})
export class ItemOperationComponent {
}
<div class="col-3 float-left d-flex h-100 action-label">
<span class="justify-content-center align-self-center">
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}}
</span>
</div>
<div *ngIf="!operation.disabled" class="col-9 float-left action-button">
<a class="btn btn-outline-secondary" href="{{operation.operationUrl}}">
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
</a>
</div>
<div *ngIf="operation.disabled" class="col-9 float-left action-button">
<span class="btn btn-danger">
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
</span>
</div>

View File

@@ -0,0 +1,13 @@
import {Component, Input} from '@angular/core';
import {ItemOperation} from './itemOperation.model';
@Component({
selector: 'ds-item-operation',
templateUrl: './item-operation.component.html'
})
export class ItemOperationComponent {
@Input() operation: ItemOperation;
}

View File

@@ -0,0 +1,12 @@
export class ItemOperation {
operationKey: string;
operationUrl: string;
disabled: boolean;
constructor(operationKey: string, operationUrl: string) {
this.operationKey = operationKey;
this.operationUrl = operationUrl;
}
}

View File

@@ -15,16 +15,7 @@
<a href="{{getItemPage()}}">{{getItemPage()}}</a>
</div>
<div *ngFor="let actionKey of actionsKeys" class="w-100 pt-3">
<div class="col-3 float-left d-flex h-100 action-label">
<span class="justify-content-center align-self-center">
{{'item.edit.tabs.status.buttons.' + actionKey + '.label' | translate}}
</span>
</div>
<div class="col-9 float-left action-button">
<a class="btn btn-outline-secondary" href="{{actions[actionKey]}}">
{{'item.edit.tabs.status.buttons.' + actionKey + '.button' | translate}}
</a>
</div>
<div *ngFor="let operation of operations" class="w-100 pt-3">
<ds-item-operation [operation]="operation"></ds-item-operation>
</div>
</div>

View File

@@ -2,6 +2,7 @@ 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';
@Component({
selector: 'ds-item-status',
@@ -35,7 +36,7 @@ export class ItemStatusComponent implements OnInit {
* The possible actions that can be performed on the item
* key: id value: url to action's component
*/
actions: any;
operations: ItemOperation[];
/**
* The keys of the actions (to loop over)
*/
@@ -57,11 +58,10 @@ export class ItemStatusComponent implements OnInit {
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
The value is supposed to be a href for the button
*/
this.actions = Object.assign({
// TODO: Create mapping component on item level
mappedCollections: this.getCurrentUrl() + '/'
});
this.actionsKeys = Object.keys(this.actions);
this.operations = [
new ItemOperation('mappedCollections',this.getCurrentUrl() + '/'),
new ItemOperation('move', this.getCurrentUrl() + '/move'),
]
}
/**

View File

@@ -5,6 +5,17 @@ import { ItemPageComponent } from './simple/item-page.component';
import {FullItemPageComponent} from './full/full-item-page.component';
import {ItemPageResolver} from './item-page.resolver';
import {AuthenticatedGuard} from '../core/auth/authenticated.guard';
import {URLCombiner} from '../core/url-combiner/url-combiner';
import {getItemModulePath} from '../app-routing.module';
export function getItemPageRoute(itemId: string) {
return new URLCombiner(getItemModulePath(), itemId).toString();
}
export function getItemEditPath(id: string) {
return new URLCombiner(getItemModulePath(),ITEM_EDIT_PATH.replace(/:id/, id)).toString()
}
const ITEM_EDIT_PATH = ':id/edit';
@NgModule({
imports: [
@@ -25,7 +36,7 @@ import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
}
},
{
path: ':id/edit',
path: ITEM_EDIT_PATH,
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
canActivate: [AuthenticatedGuard]
}

View File

@@ -1,4 +1,4 @@
import { NgModule } from '@angular/core';
import {ModuleWithProviders, NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {CoreModule} from '../core/core.module';
import {SharedModule} from '../shared/shared.module';
@@ -9,7 +9,7 @@ import { ItemSearchResultListElementComponent } from '../shared/object-list/sear
import {CollectionSearchResultListElementComponent} from '../shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component';
import {CommunitySearchResultListElementComponent} from '../shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component';
import {ItemSearchResultGridElementComponent} from '../shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component';
import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'
import {CommunitySearchResultGridElementComponent} from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component';
import {CollectionSearchResultGridElementComponent} from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component';
import {SearchService} from './search-service/search.service';
import {SearchSidebarComponent} from './search-sidebar/search-sidebar.component';
@@ -33,6 +33,13 @@ const effects = [
SearchSidebarEffects
];
const PROVIDERS = [
SearchService,
SearchSidebarService,
SearchFilterService,
SearchConfigurationService
];
@NgModule({
imports: [
SearchPageRoutingModule,
@@ -65,10 +72,7 @@ const effects = [
SearchBooleanFilterComponent,
],
providers: [
SearchService,
SearchSidebarService,
SearchFilterService,
SearchConfigurationService
...PROVIDERS
],
entryComponents: [
ItemSearchResultListElementComponent,
@@ -89,4 +93,12 @@ const effects = [
* This module handles all components and pipes that are necessary for the search page
*/
export class SearchPageModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: CoreModule,
providers: [
...PROVIDERS
]
};
}
}

View File

@@ -3,6 +3,10 @@ import { RouterModule } from '@angular/router';
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
const ITEM_MODULE_PATH = 'items';
export function getItemModulePath() {
return `/${ITEM_MODULE_PATH}`;
}
@NgModule({
imports: [
RouterModule.forRoot([
@@ -10,7 +14,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
{ path: 'communities', loadChildren: './+community-page/community-page.module#CommunityPageModule' },
{ path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
{ path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },

View File

@@ -1,9 +1,8 @@
import { Inject, Injectable } from '@angular/core';
import {Injectable} from '@angular/core';
import {Store} from '@ngrx/store';
import {Observable} from 'rxjs/Observable';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import {isNotEmpty, isNotEmptyOperator} from '../../shared/empty.util';
import {BrowseService} from '../browse/browse.service';
import {RemoteDataBuildService} from '../cache/builders/remote-data-build.service';
import {NormalizedItem} from '../cache/models/normalized-item.model';
@@ -15,7 +14,11 @@ import { URLCombiner } from '../url-combiner/url-combiner';
import {DataService} from './data.service';
import {RequestService} from './request.service';
import {HALEndpointService} from '../shared/hal-endpoint.service';
import { FindAllOptions } from './request.models';
import {FindAllOptions, PostRequest, RestRequest} from './request.models';
import {distinctUntilChanged, map} from 'rxjs/operators';
import {RestResponse} from '../cache/response-cache.models';
import {configureRequest, getResponseFromSelflink} from '../shared/operators';
import {ResponseCacheEntry} from '../cache/response-cache.reducer';
@Injectable()
export class ItemDataService extends DataService<NormalizedItem, Item> {
@@ -48,4 +51,22 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
.distinctUntilChanged();
}
public getMoveItemEndpoint(itemId: string, collectionId?: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath).pipe(
map((endpoint: string) => this.getFindByIDHref(endpoint, itemId)),
map((endpoint: string) => `${endpoint}/owningCollection/move/${collectionId ? `/${collectionId}` : ''}`)
);
}
public moveToCollection(itemId: string, collectionId: string): Observable<RestResponse> {
return this.getMoveItemEndpoint(itemId, collectionId).pipe(
// isNotEmptyOperator(),
distinctUntilChanged(),
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)),
configureRequest(this.requestService),
map((request: RestRequest) => request.href),
getResponseFromSelflink(this.responseCache),
map((responseCacheEntry: ResponseCacheEntry) => responseCacheEntry.response)
);
}
}

View File

@@ -159,6 +159,15 @@ export class InputSuggestionsComponent {
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
*/