Merge pull request #534 from atmire/reorder-name-variants

Reordering related entities in the submission
This commit is contained in:
Tim Donohue
2020-01-16 08:49:29 -06:00
committed by GitHub
48 changed files with 568 additions and 274 deletions

View File

@@ -11,14 +11,14 @@ import { Site } from '../core/shared/site.model';
})
export class HomePageComponent implements OnInit {
site$:Observable<Site>;
site$: Observable<Site>;
constructor(
private route:ActivatedRoute,
private route: ActivatedRoute,
) {
}
ngOnInit():void {
ngOnInit(): void {
this.site$ = this.route.data.pipe(
map((data) => data.site as Site),
);

View File

@@ -10,7 +10,7 @@ import { take } from 'rxjs/operators';
*/
@Injectable()
export class HomePageResolver implements Resolve<Site> {
constructor(private siteService:SiteDataService) {
constructor(private siteService: SiteDataService) {
}
/**
@@ -19,7 +19,7 @@ export class HomePageResolver implements Resolve<Site> {
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<Site> Emits the found Site object, or an error if something went wrong
*/
resolve(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<Site> | Promise<Site> | Site {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Site> | Promise<Site> | Site {
return this.siteService.find().pipe(take(1));
}
}

View File

@@ -70,5 +70,4 @@ export class EditRelationshipComponent implements OnChanges {
canUndo(): boolean {
return this.fieldUpdate.changeType >= 0;
}
}

View File

@@ -18,7 +18,7 @@ export class LookupGuard implements CanActivate {
constructor(private dsoService: DsoRedirectDataService) {
}
canActivate(route: ActivatedRouteSnapshot, state:RouterStateSnapshot): Observable<boolean> {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const params = this.getLookupParams(route);
return this.dsoService.findById(params.id, params.type).pipe(
map((response: RemoteData<FindByIDRequest>) => response.hasFailed)

View File

@@ -5,26 +5,21 @@ import { SharedModule } from '../shared/shared.module';
import { SearchPageRoutingModule } from './search-page-routing.module';
import { SearchComponent } from './search.component';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SidebarEffects } from '../shared/sidebar/sidebar-effects.service';
import { EffectsModule } from '@ngrx/effects';
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
import { SearchTrackerComponent } from './search-tracker.component';
import { StatisticsModule } from '../statistics/statistics.module';
import { SearchPageComponent } from './search-page.component';
import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service';
import { StatisticsModule } from '../statistics/statistics.module';
import { SearchTrackerComponent } from './search-tracker.component';
import { SearchFilterService } from '../core/shared/search/search-filter.service';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
const effects = [
SidebarEffects
];
const components = [
SearchPageComponent,
SearchComponent,
ConfigurationSearchPageComponent,
SearchTrackerComponent,
SearchTrackerComponent
];
@NgModule({
@@ -32,7 +27,6 @@ const components = [
SearchPageRoutingModule,
CommonModule,
SharedModule,
EffectsModule.forFeature(effects),
CoreModule.forRoot(),
StatisticsModule.forRoot(),
],

View File

@@ -9,10 +9,10 @@ import { RouteService } from '../core/services/route.service';
import { hasValue } from '../shared/empty.util';
import { SearchSuccessResponse } from '../core/cache/response.models';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { Router } from '@angular/router';
import { SearchService } from '../core/shared/search/search.service';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { SearchQueryResponse } from '../shared/search/search-query-response.model';
import { Router } from '@angular/router';
/**
* This component triggers a page view statistic
@@ -42,7 +42,7 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit {
super(service, sidebarService, windowService, searchConfigService, routeService, router);
}
ngOnInit():void {
ngOnInit(): void {
// super.ngOnInit();
this.getSearchOptions().pipe(
switchMap((options) => this.service.searchEntries(options)
@@ -62,7 +62,7 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit {
.subscribe((entry) => {
const config: PaginatedSearchOptions = entry.searchOptions;
const searchQueryResponse: SearchQueryResponse = entry.response;
const filters:Array<{ filter: string, operator: string, value: string, label: string; }> = [];
const filters: Array<{ filter: string, operator: string, value: string, label: string; }> = [];
const appliedFilters = searchQueryResponse.appliedFilters || [];
for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) {
const appliedFilter = appliedFilters[i];

View File

@@ -46,9 +46,9 @@
[scopes]="(scopeListRD$ | async)"
[inPlaceSearch]="inPlaceSearch">
</ds-search-form>
<div class="row mb-3 mb-md-1">
<div class="labels col-sm-9 offset-sm-3">
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
</div>
<div class="row mb-3 mb-md-1">
<div class="labels col-sm-9 offset-sm-3">
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
</div>
</div>
</ng-template>

View File

@@ -11,9 +11,9 @@ import { hasValue, isNotEmpty } from '../shared/empty.util';
import { getSucceededRemoteData } from '../core/shared/operators';
import { RouteService } from '../core/services/route.service';
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { SearchResult } from '../shared/search/search-result.model';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { SearchResult } from '../shared/search/search-result.model';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { SearchService } from '../core/shared/search/search.service';
import { currentPath } from '../shared/utils/route.utils';
import { Router } from '@angular/router';

View File

@@ -56,7 +56,7 @@ export class AuthInterceptor implements HttpInterceptor {
return http.url && http.url.endsWith('/authn/logout');
}
private makeAuthStatusObject(authenticated:boolean, accessToken?: string, error?: string): AuthStatus {
private makeAuthStatusObject(authenticated: boolean, accessToken?: string, error?: string): AuthStatus {
const authStatus = new AuthStatus();
authStatus.id = null;
authStatus.okay = true;

View File

@@ -17,5 +17,5 @@ export interface ChangeAnalyzer<T extends CacheableObject> {
* @param {NormalizedObject} object2
* The second object to compare
*/
diff(object1: T | NormalizedObject<T>, object2: T | NormalizedObject<T>): Operation[];
diff(object1: T | NormalizedObject<T>, object2: T | NormalizedObject<T>): Operation[];
}

View File

@@ -17,6 +17,7 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { Item } from '../shared/item.model';
import * as uuidv4 from 'uuid/v4';
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
const endpoint = 'https://rest.api/core';
@@ -191,8 +192,7 @@ describe('DataService', () => {
dso2.self = selfLink;
dso2.metadata = [{ key: 'dc.title', value: name2 }];
spyOn(service, 'findByHref').and.returnValues(observableOf(dso));
spyOn(objectCache, 'getObjectBySelfLink').and.returnValues(observableOf(dso));
spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(dso));
spyOn(objectCache, 'addPatch');
});

View File

@@ -37,7 +37,7 @@ import { Operation } from 'fast-json-patch';
import { ObjectCacheService } from '../cache/object-cache.service';
import { DSpaceObject } from '../shared/dspace-object.model';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { configureRequest, getResponseFromEntry } from '../shared/operators';
import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
import { ErrorResponse, RestResponse } from '../cache/response.models';
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
@@ -248,8 +248,11 @@ export abstract class DataService<T extends CacheableObject> {
* @param {DSpaceObject} object The given object
*/
update(object: T): Observable<RemoteData<T>> {
const oldVersion$ = this.objectCache.getObjectBySelfLink(object.self);
return oldVersion$.pipe(take(1), mergeMap((oldVersion: NormalizedObject<T>) => {
const oldVersion$ = this.findByHref(object.self);
return oldVersion$.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
mergeMap((oldVersion: T) => {
const operations = this.comparator.diff(oldVersion, object);
if (isNotEmpty(operations)) {
this.objectCache.addPatch(object.self, operations);
@@ -257,7 +260,6 @@ export abstract class DataService<T extends CacheableObject> {
return this.findByHref(object.self);
}
));
}
/**

View File

@@ -123,8 +123,8 @@ describe('RelationshipService', () => {
it('should clear the related items their cache', () => {
expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1.self);
expect(objectCache.remove).toHaveBeenCalledWith(item.self);
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.self);
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self);
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.uuid);
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.uuid);
});
});

View File

@@ -1,46 +1,35 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
import {
configureRequest,
getRemoteDataPayload,
getResponseFromEntry,
getSucceededRemoteData
} from '../shared/operators';
import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models';
import { Observable } from 'rxjs/internal/Observable';
import { RestResponse } from '../cache/response.models';
import { Item } from '../shared/item.model';
import { Relationship } from '../shared/item-relationships/relationship.model';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
import { RemoteData } from './remote-data';
import { MemoizedSelector, select, Store } from '@ngrx/store';
import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
import { compareArraysUsingIds, paginatedRelationsToItems, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils';
import { AppState, keySelector } from '../../app.reducer';
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
import { RemoveNameVariantAction, SetNameVariantAction } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions';
import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
import { SearchParam } from '../cache/models/search-param.model';
import { ObjectCacheService } from '../cache/object-cache.service';
import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models';
import { RestResponse } from '../cache/response.models';
import { CoreState } from '../core.reducers';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
import { RemoteData, RemoteDataState } from './remote-data';
import { PaginatedList } from './paginated-list';
import { ItemDataService } from './item-data.service';
import {
compareArraysUsingIds,
paginatedRelationsToItems,
relationsToItems
} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
import { ObjectCacheService } from '../cache/object-cache.service';
import { Relationship } from '../shared/item-relationships/relationship.model';
import { Item } from '../shared/item.model';
import { DataService } from './data.service';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { MemoizedSelector, select, Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { SearchParam } from '../cache/models/search-param.model';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { AppState, keySelector } from '../../app.reducer';
import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
import {
RemoveNameVariantAction,
SetNameVariantAction
} from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions';
import { RequestService } from './request.service';
import { Observable } from 'rxjs/internal/Observable';
const relationshipListsStateSelector = (state: AppState) => state.relationshipLists;
@@ -140,9 +129,9 @@ export class RelationshipService extends DataService<Relationship> {
this.findById(relationshipId).pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
switchMap((relationship: Relationship) => combineLatest(
relationship.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()),
relationship.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload())
switchMap((rel: Relationship) => combineLatest(
rel.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()),
rel.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload())
)
),
take(1)
@@ -158,10 +147,10 @@ export class RelationshipService extends DataService<Relationship> {
*/
private removeRelationshipItemsFromCache(item) {
this.objectCache.remove(item.self);
this.requestService.removeByHrefSubstring(item.self);
this.requestService.removeByHrefSubstring(item.uuid);
combineLatest(
this.objectCache.hasBySelfLinkObservable(item.self),
this.requestService.hasByHrefObservable(item.self)
this.requestService.hasByHrefObservable(item.uuid)
).pipe(
filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC),
take(1),
@@ -367,7 +356,7 @@ export class RelationshipService extends DataService<Relationship> {
* @param nameVariant The name variant to set for the matching relationship
*/
public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable<RemoteData<Relationship>> {
return this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel)
const update$: Observable<RemoteData<Relationship>> = this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel)
.pipe(
switchMap((relation: Relationship) =>
relation.relationshipType.pipe(
@@ -388,14 +377,44 @@ export class RelationshipService extends DataService<Relationship> {
}
return this.update(updatedRelationship);
}),
// skipWhile((relationshipRD: RemoteData<Relationship>) => !relationshipRD.isSuccessful)
tap((relationshipRD: RemoteData<Relationship>) => {
if (relationshipRD.hasSucceeded) {
this.removeRelationshipItemsFromCache(item1);
this.removeRelationshipItemsFromCache(item2);
}
}),
)
);
update$.pipe(
filter((relationshipRD: RemoteData<Relationship>) => relationshipRD.state === RemoteDataState.RequestPending),
take(1),
).subscribe(() => {
this.removeRelationshipItemsFromCache(item1);
this.removeRelationshipItemsFromCache(item2);
});
return update$
}
/**
* Method to update the the right or left place of a relationship
* The useLeftItem field in the reorderable relationship determines which place should be updated
* @param reoRel
*/
public updatePlace(reoRel: ReorderableRelationship): Observable<RemoteData<Relationship>> {
let updatedRelationship;
if (reoRel.useLeftItem) {
updatedRelationship = Object.assign(new Relationship(), reoRel.relationship, { rightPlace: reoRel.newIndex });
} else {
updatedRelationship = Object.assign(new Relationship(), reoRel.relationship, { leftPlace: reoRel.newIndex });
}
const update$ = this.update(updatedRelationship);
update$.pipe(
filter((relationshipRD: RemoteData<Relationship>) => relationshipRD.state === RemoteDataState.ResponsePending),
take(1),
).subscribe((relationshipRD: RemoteData<Relationship>) => {
if (relationshipRD.state === RemoteDataState.ResponsePending) {
this.removeRelationshipItemsFromCacheByRelationship(reoRel.relationship.id);
}
});
return update$;
}
}

View File

@@ -19,12 +19,12 @@ import { PaginatedList } from './paginated-list';
import { RemoteData } from './remote-data';
describe('SiteDataService', () => {
let scheduler:TestScheduler;
let service:SiteDataService;
let halService:HALEndpointService;
let requestService:RequestService;
let rdbService:RemoteDataBuildService;
let objectCache:ObjectCacheService;
let scheduler: TestScheduler;
let service: SiteDataService;
let halService: HALEndpointService;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
const testObject = Object.assign(new Site(), {
uuid: '9b4f22f4-164a-49db-8817-3316b6ee5746',
@@ -33,7 +33,7 @@ describe('SiteDataService', () => {
const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
const options = Object.assign(new FindListOptions(), {});
const getRequestEntry$ = (successful:boolean, statusCode:number, statusText:string) => {
const getRequestEntry$ = (successful: boolean, statusCode: number, statusText: string) => {
return observableOf({
response: new RestResponse(successful, statusCode, statusText)
} as RequestEntry);

View File

@@ -22,47 +22,41 @@ import { getSucceededRemoteData } from '../shared/operators';
* Service responsible for handling requests related to the Site object
*/
@Injectable()
export class SiteDataService extends DataService<Site> {
export class SiteDataService extends DataService<Site> {
protected linkPath = 'sites';
protected forceBypassCache = false;
constructor(
protected requestService:RequestService,
protected rdbService:RemoteDataBuildService,
protected dataBuildService:NormalizedObjectBuildService,
protected store:Store<CoreState>,
protected objectCache:ObjectCacheService,
protected halService:HALEndpointService,
protected notificationsService:NotificationsService,
protected http:HttpClient,
protected comparator:DSOChangeAnalyzer<Site>,
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected dataBuildService: NormalizedObjectBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DSOChangeAnalyzer<Site>,
) {
super();
}
/**
* Get the endpoint for browsing the site object
* @param {FindListOptions} options
* @param {Observable<string>} linkPath
*/
getBrowseEndpoint(options:FindListOptions, linkPath?:string):Observable<string> {
getBrowseEndpoint(options: FindListOptions, linkPath?: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath);
}
/**
* Retrieve the Site Object
*/
find():Observable<Site> {
find(): Observable<Site> {
return this.findAll().pipe(
getSucceededRemoteData(),
map((remoteData:RemoteData<PaginatedList<Site>>) => remoteData.payload),
map((list:PaginatedList<Site>) => list.page[0])
map((remoteData: RemoteData<PaginatedList<Site>>) => remoteData.payload),
map((list: PaginatedList<Site>) => list.page[0])
);
}
}

View File

@@ -89,9 +89,9 @@ export class SearchService implements OnDestroy {
}
}
getEndpoint(searchOptions?:PaginatedSearchOptions):Observable<string> {
getEndpoint(searchOptions?: PaginatedSearchOptions): Observable<string> {
return this.halService.getEndpoint(this.searchLinkPath).pipe(
map((url:string) => {
map((url: string) => {
if (hasValue(searchOptions)) {
return (searchOptions as PaginatedSearchOptions).toRestUrl(url);
} else {
@@ -117,16 +117,15 @@ export class SearchService implements OnDestroy {
* @param responseMsToLive The amount of milliseconds for the response to live in cache
* @returns {Observable<RequestEntry>} Emits an observable with the request entries
*/
searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?:number)
:Observable<{searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry}> {
searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number): Observable<{searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry}> {
const hrefObs = this.getEndpoint(searchOptions);
const requestObs = hrefObs.pipe(
map((url:string) => {
map((url: string) => {
const request = new this.request(this.requestService.generateRequestId(), url);
const getResponseParserFn:() => GenericConstructor<ResponseParsingService> = () => {
const getResponseParserFn: () => GenericConstructor<ResponseParsingService> = () => {
return this.parser;
};
@@ -139,8 +138,8 @@ export class SearchService implements OnDestroy {
configureRequest(this.requestService),
);
return requestObs.pipe(
switchMap((request:RestRequest) => this.requestService.getByHref(request.href)),
map(((requestEntry:RequestEntry) => ({
switchMap((request: RestRequest) => this.requestService.getByHref(request.href)),
map(((requestEntry: RequestEntry) => ({
searchOptions: searchOptions,
requestEntry: requestEntry
})))
@@ -152,16 +151,15 @@ export class SearchService implements OnDestroy {
* @param searchEntries: The request entries from the search method
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
*/
getPaginatedResults(searchEntries:Observable<{ searchOptions:PaginatedSearchOptions, requestEntry:RequestEntry }>)
:Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
const requestEntryObs:Observable<RequestEntry> = searchEntries.pipe(
getPaginatedResults(searchEntries: Observable<{ searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry }>): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
const requestEntryObs: Observable<RequestEntry> = searchEntries.pipe(
map((entry) => entry.requestEntry),
);
// get search results from response cache
const sqrObs:Observable<SearchQueryResponse> = requestEntryObs.pipe(
const sqrObs: Observable<SearchQueryResponse> = requestEntryObs.pipe(
filterSuccessfulResponses(),
map((response:SearchSuccessResponse) => response.results),
map((response: SearchSuccessResponse) => response.results),
);
// turn dspace href from search results to effective list of DSpaceObjects

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { metadataRepresentationComponent } from '../../../../shared/metadata-representation/metadata-representation.decorator';
import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model';
import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component';

View File

@@ -53,16 +53,15 @@
<div *ngIf="hasRelationLookup" class="mt-3">
<ul class="list-unstyled">
<li *ngFor="let value of ( selectedValues$ | async)">
<button type="button" class="close float-left" aria-label="Close button"
(click)="removeSelection(value.selectedResult)">
<span aria-hidden="true">&times;</span>
</button>
<span class="d-inline-block align-middle ml-1">
<ds-metadata-representation-loader [mdRepresentation]="value.mdRep"></ds-metadata-representation-loader>
</span>
</li>
<ul class="list-unstyled" cdkDropList (cdkDropListDropped)="moveSelection($event)">
<ds-existing-metadata-list-element cdkDrag
*ngFor="let reorderable of reorderables; trackBy: trackReorderable"
[reoRel]="reorderable"
[submissionItem]="item"
[listId]="listId"
[metadataFields]="model.metadataFields"
[relationshipOptions]="model.relationship">
</ds-existing-metadata-list-element>
</ul>
</div>
</div>

View File

@@ -1,12 +1,13 @@
import {
ChangeDetectionStrategy,
ChangeDetectionStrategy, ChangeDetectorRef,
Component,
ComponentFactoryResolver,
ContentChildren,
EventEmitter,
Input,
NgZone,
OnChanges, OnDestroy,
OnChanges,
OnDestroy,
OnInit,
Output,
QueryList,
@@ -49,7 +50,10 @@ import {
DynamicNGBootstrapTimePickerComponent
} from '@ng-dynamic-forms/ui-ng-bootstrap';
import { TranslateService } from '@ngx-translate/core';
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
import {
Reorderable,
ReorderableRelationship
} from './existing-metadata-list-element/existing-metadata-list-element.component';
import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model';
import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
@@ -71,9 +75,8 @@ import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-a
import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components';
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-group/dynamic-relation-group.model';
import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component';
import { map, switchMap, take, tap } from 'rxjs/operators';
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer';
import { map, startWith, switchMap, find } from 'rxjs/operators';
import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
import { SearchResult } from '../../../search/search-result.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
@@ -82,23 +85,18 @@ import { SelectableListService } from '../../../object-list/selectable-list/sele
import { DsDynamicDisabledComponent } from './models/disabled/dynamic-disabled.component';
import { DYNAMIC_FORM_CONTROL_TYPE_DISABLED } from './models/disabled/dynamic-disabled.model';
import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component';
import {
getAllSucceededRemoteData,
getRemoteDataPayload,
getSucceededRemoteData
} from '../../../../core/shared/operators';
import { getAllSucceededRemoteData, getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { RemoteData } from '../../../../core/data/remote-data';
import { Item } from '../../../../core/shared/item.model';
import { ItemDataService } from '../../../../core/data/item-data.service';
import { RemoveRelationshipAction } from './relation-lookup-modal/relationship.actions';
import { Store } from '@ngrx/store';
import { AppState } from '../../../../app.reducer';
import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service';
import { SubmissionObject } from '../../../../core/submission/models/submission-object.model';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { MetadataValue } from '../../../../core/shared/metadata.models';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<DynamicFormControl> | null {
switch (model.type) {
@@ -182,16 +180,14 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
@Input() hasErrorMessaging = false;
@Input() layout = null as DynamicFormLayout;
@Input() model: any;
relationships$: Observable<Array<SearchResult<Item>>>;
reorderables$: Observable<ReorderableRelationship[]>;
reorderables: ReorderableRelationship[];
hasRelationLookup: boolean;
modalRef: NgbModalRef;
item: Item;
listId: string;
searchConfig: string;
selectedValues$: Observable<Array<{
selectedResult: SearchResult<Item>,
mdRep: MetadataRepresentation
}>>;
/**
* List of subscriptions to unsubscribe from
*/
@@ -224,7 +220,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
private relationshipService: RelationshipService,
private zone: NgZone,
private store: Store<AppState>,
private submissionObjectService: SubmissionObjectDataService
private submissionObjectService: SubmissionObjectDataService,
private ref: ChangeDetectorRef
) {
super(componentFactoryResolver, layoutService, validationService, dynamicFormInstanceService);
@@ -235,44 +232,59 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
*/
ngOnInit(): void {
this.hasRelationLookup = hasValue(this.model.relationship);
this.reorderables = [];
if (this.hasRelationLookup) {
this.listId = 'list-' + this.model.relationship.relationshipType;
const item$ = this.submissionObjectService
.findById(this.model.submissionId).pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload(),
switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable<RemoteData<Item>>)
.pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload()
)
)
);
this.subs.push(item$.subscribe((item) => this.item = item));
this.reorderables$ = item$.pipe(
switchMap((item) => this.relationService.getItemRelationshipsByLabel(item, this.model.relationship.relationshipType)
.pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload(),
map((relationshipList: PaginatedList<Relationship>) => relationshipList.page),
startWith([]),
switchMap((relationships: Relationship[]) =>
observableCombineLatest(
relationships.map((relationship: Relationship) =>
relationship.leftItem.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
map((leftItem: Item) => {
return new ReorderableRelationship(relationship, leftItem.uuid !== this.item.uuid)
}),
)
))),
map((relationships: ReorderableRelationship[]) =>
relationships
.sort((a: Reorderable, b: Reorderable) => {
return Math.sign(a.getPlace() - b.getPlace());
})
)
)
)
);
this.subs.push(this.reorderables$.subscribe((rs) => {
this.reorderables = rs;
this.ref.detectChanges();
}));
this.relationService.getRelatedItemsByLabel(this.item, this.model.relationship.relationshipType).pipe(
map((items: RemoteData<PaginatedList<Item>>) => items.payload.page.map((item) => Object.assign(new ItemSearchResult(), { indexableObject: item }))),
).subscribe((relatedItems: Array<SearchResult<Item>>) => this.selectableListService.select(this.listId, relatedItems));
this.relationships$ = this.selectableListService.getSelectableList(this.listId).pipe(
map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : []),
) as Observable<Array<SearchResult<Item>>>;
this.selectedValues$ =
observableCombineLatest(item$, this.relationships$).pipe(
map(([item, relatedItems]: [Item, Array<SearchResult<DSpaceObject>>]) => {
return relatedItems
.map((element: SearchResult<Item>) => {
const relationMD: MetadataValue = item.firstMetadata(this.model.relationship.metadataField, { value: element.indexableObject.uuid });
if (hasValue(relationMD)) {
const metadataRepresentationMD: MetadataValue = item.firstMetadata(this.model.metadataFields, { authority: relationMD.authority });
return {
selectedResult: element,
mdRep: Object.assign(
new ItemMetadataRepresentation(metadataRepresentationMD),
element.indexableObject
)
};
}
}).filter(hasValue)
}
)
);
}
}
@@ -334,12 +346,29 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
}
/**
* Method to remove a selected relationship from the item
* @param object The second item in the relationship, the submitted item being the first
* Method to move a relationship inside the list of relationships
* This will update the view and update the right or left place field of the relationships in the list
* @param event
*/
removeSelection(object: SearchResult<Item>) {
this.selectableListService.deselectSingle(this.listId, object);
this.store.dispatch(new RemoveRelationshipAction(this.item, object.indexableObject, this.model.relationship.relationshipType))
moveSelection(event: CdkDragDrop<Relationship>) {
this.zone.runOutsideAngular(() => {
moveItemInArray(this.reorderables, event.previousIndex, event.currentIndex);
const reorderables: Reorderable[] = this.reorderables.map((reo: Reorderable, index: number) => {
reo.oldIndex = reo.getPlace();
reo.newIndex = index;
return reo;
}
);
observableCombineLatest(
reorderables.map((rel: ReorderableRelationship) => {
if (rel.oldIndex !== rel.newIndex) {
return this.relationshipService.updatePlace(rel);
} else {
return observableOf(undefined) as Observable<RemoteData<Relationship>>;
}
})
).subscribe();
})
}
/**
@@ -350,4 +379,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
/**
* Prevent unnecessary rerendering so fields don't lose focus
*/
trackReorderable(index, reorderable: Reorderable) {
return hasValue(reorderable) ? reorderable.getId() : undefined;
}
}

View File

@@ -0,0 +1,11 @@
<li *ngIf="metadataRepresentation">
<button type="button" class="close float-left" aria-label="Move button" cdkDragHandle>
<i aria-hidden="true" class="fas fa-arrows-alt fa-xs"></i>
</button>
<button type="button" class="close float-left" aria-label="Close button" (click)="removeSelection()">
<span aria-hidden="true">&times;</span>
</button>
<span class="d-inline-block align-middle ml-1">
<ds-metadata-representation-loader [mdRepresentation]="metadataRepresentation"></ds-metadata-representation-loader>
</span>
</li>

View File

@@ -0,0 +1,92 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ExistingMetadataListElementComponent, Reorderable, ReorderableRelationship } from './existing-metadata-list-element.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
import { select, Store } from '@ngrx/store';
import { Item } from '../../../../../core/shared/item.model';
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
import { RelationshipOptions } from '../../models/relationship-options.model';
import { createSuccessfulRemoteDataObject$ } from '../../../../testing/utils';
import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions';
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
describe('ExistingMetadataListElementComponent', () => {
let component: ExistingMetadataListElementComponent;
let fixture: ComponentFixture<ExistingMetadataListElementComponent>;
let selectionService;
let store;
let listID;
let submissionItem;
let relationship;
let reoRel;
let metadataFields;
let relationshipOptions;
let uuid1;
let uuid2;
let relatedItem;
let leftItemRD$;
let rightItemRD$;
let relatedSearchResult;
function init() {
uuid1 = '91ce578d-2e63-4093-8c73-3faafd716000';
uuid2 = '0e9dba1c-e1c3-4e05-a539-446f08ef57a7';
selectionService = jasmine.createSpyObj('selectionService', ['deselectSingle']);
store = jasmine.createSpyObj('store', ['dispatch']);
listID = '1234-listID';
submissionItem = Object.assign(new Item(), { uuid: uuid1 });
metadataFields = ['dc.contributor.author'];
relationshipOptions = Object.assign(new RelationshipOptions(), { relationshipType: 'isPublicationOfAuthor', filter: 'test.filter', searchConfiguration: 'personConfiguration', nameVariants: true })
relatedItem = Object.assign(new Item(), { uuid: uuid2 });
leftItemRD$ = createSuccessfulRemoteDataObject$(relatedItem);
rightItemRD$ = createSuccessfulRemoteDataObject$(submissionItem);
relatedSearchResult = Object.assign(new ItemSearchResult(), { indexableObject: relatedItem });
relationship = Object.assign(new Relationship(), { leftItem: leftItemRD$, rightItem: rightItemRD$ });
reoRel = new ReorderableRelationship(relationship, true);
}
beforeEach(async(() => {
init();
TestBed.configureTestingModule({
declarations: [ExistingMetadataListElementComponent],
providers: [
{ provide: SelectableListService, useValue: selectionService },
{ provide: Store, useValue: store },
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ExistingMetadataListElementComponent);
component = fixture.componentInstance;
component.listId = listID;
component.submissionItem = submissionItem;
component.reoRel = reoRel;
component.metadataFields = metadataFields;
component.relationshipOptions = relationshipOptions;
fixture.detectChanges();
component.ngOnChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('removeSelection', () => {
it('should deselect the object in the selectable list service', () => {
component.removeSelection();
expect(selectionService.deselectSingle).toHaveBeenCalledWith(listID, relatedSearchResult);
});
it('should dispatch a RemoveRelationshipAction', () => {
component.removeSelection();
const action = new RemoveRelationshipAction(submissionItem, relatedItem, relationshipOptions.relationshipType);
expect(store.dispatch).toHaveBeenCalledWith(action);
});
})
});

View File

@@ -0,0 +1,123 @@
import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { Item } from '../../../../../core/shared/item.model';
import { MetadataRepresentation } from '../../../../../core/shared/metadata-representation/metadata-representation.model';
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators';
import { hasValue, isNotEmpty } from '../../../../empty.util';
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
import { MetadataValue } from '../../../../../core/shared/metadata.models';
import { ItemMetadataRepresentation } from '../../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { RelationshipOptions } from '../../models/relationship-options.model';
import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions';
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
import { Store } from '@ngrx/store';
import { AppState } from '../../../../../app.reducer';
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
// tslint:disable:max-classes-per-file
/**
* Abstract class that defines objects that can be reordered
*/
export abstract class Reorderable {
constructor(public oldIndex?: number, public newIndex?: number) {
}
abstract getId(): string;
abstract getPlace(): number;
}
/**
* Represents a single relationship that can be reordered in a list of multiple relationships
*/
export class ReorderableRelationship extends Reorderable {
relationship: Relationship;
useLeftItem: boolean;
constructor(relationship: Relationship, useLeftItem: boolean, oldIndex?: number, newIndex?: number) {
super(oldIndex, newIndex);
this.relationship = relationship;
this.useLeftItem = useLeftItem;
}
getId(): string {
return this.relationship.id;
}
getPlace(): number {
if (this.useLeftItem) {
return this.relationship.rightPlace
} else {
return this.relationship.leftPlace
}
}
}
/**
* Represents a single existing relationship value as metadata in submission
*/
@Component({
selector: 'ds-existing-metadata-list-element',
templateUrl: './existing-metadata-list-element.component.html',
styleUrls: ['./existing-metadata-list-element.component.scss']
})
export class ExistingMetadataListElementComponent implements OnChanges, OnDestroy {
@Input() listId: string;
@Input() submissionItem: Item;
@Input() reoRel: ReorderableRelationship;
@Input() metadataFields: string[];
@Input() relationshipOptions: RelationshipOptions;
metadataRepresentation: MetadataRepresentation;
relatedItem: Item;
/**
* List of subscriptions to unsubscribe from
*/
private subs: Subscription[] = [];
constructor(
private selectableListService: SelectableListService,
private store: Store<AppState>
) {
}
ngOnChanges() {
const item$ = this.reoRel.useLeftItem ?
this.reoRel.relationship.leftItem : this.reoRel.relationship.rightItem;
this.subs.push(item$.pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload(),
filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid))
).subscribe((item: Item) => {
this.relatedItem = item;
const relationMD: MetadataValue = this.submissionItem.firstMetadata(this.relationshipOptions.metadataField, { value: this.relatedItem.uuid });
if (hasValue(relationMD)) {
const metadataRepresentationMD: MetadataValue = this.submissionItem.firstMetadata(this.metadataFields, { authority: relationMD.authority });
this.metadataRepresentation = Object.assign(
new ItemMetadataRepresentation(metadataRepresentationMD),
this.relatedItem
)
}
}));
}
/**
* Removes the selected relationship from the list
*/
removeSelection() {
this.selectableListService.deselectSingle(this.listId, Object.assign(new ItemSearchResult(), { indexableObject: this.relatedItem }));
this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, this.relatedItem, this.relationshipOptions.relationshipType))
}
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
}
// tslint:enable:max-classes-per-file

View File

@@ -135,7 +135,6 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
this.externalSourcesRD$ = this.externalSourceService.findAll();
this.setTotals();
// this.setExistingNameVariants();
}
close() {

View File

@@ -3,6 +3,7 @@ import { Actions, Effect, ofType } from '@ngrx/effects';
import { debounceTime, map, mergeMap, take, tap } from 'rxjs/operators';
import { BehaviorSubject } from 'rxjs';
import { RelationshipService } from '../../../../../core/data/relationship.service';
import { getSucceededRemoteData } from '../../../../../core/shared/operators';
import { AddRelationshipAction, RelationshipAction, RelationshipActionTypes, RemoveRelationshipAction, UpdateRelationshipAction } from './relationship.actions';
import { Item } from '../../../../../core/shared/item.model';
import { hasNoValue, hasValue, hasValueOperator } from '../../../../empty.util';
@@ -88,7 +89,7 @@ export class RelationshipEffects {
this.nameVariantUpdates[identifier] = nameVariant;
} else {
this.relationshipService.updateNameVariant(item1, item2, relationshipType, nameVariant)
.pipe()
.pipe(getSucceededRemoteData())
.subscribe();
}
}

View File

@@ -78,7 +78,7 @@ describe('LangSwitchComponent', () => {
}).compileComponents()
.then(() => {
translate = TestBed.get(TranslateService);
translate.addLangs(mockConfig.languages.filter((langConfig:LangConfig) => langConfig.active === true).map((a) => a.code));
translate.addLangs(mockConfig.languages.filter((langConfig: LangConfig) => langConfig.active === true).map((a) => a.code));
translate.setDefaultLang('en');
translate.use('en');
http = TestBed.get(HttpTestingController);

View File

@@ -1,5 +1,5 @@
/* tslint:disable:no-empty */
export class AngularticsMock {
public eventTrack(action, properties) { }
public startTracking():void {}
public startTracking(): void {}
}

View File

@@ -24,6 +24,7 @@ import { getSucceededRemoteData } from '../../../../../core/shared/operators';
import { InputSuggestion } from '../../../../input-suggestions/input-suggestions.model';
import { SearchOptions } from '../../../search-options.model';
import { SEARCH_CONFIG_SERVICE } from '../../../../../+my-dspace-page/my-dspace-page.component';
import { currentPath } from '../../../../utils/route.utils';
@Component({
selector: 'ds-search-facet-filter',
@@ -185,7 +186,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
*/
public getSearchLink(): string {
if (this.inPlaceSearch) {
return '';
return currentPath(this.router);
}
return this.searchService.getSearchLink();
}

View File

@@ -40,7 +40,7 @@ describe('SearchLabelComponent', () => {
providers: [
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) },
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
{ provide: Router, useValue: {} }
{ provide: Router, useValue: {}}
// { provide: SearchConfigurationService, useValue: {getCurrentFrontendFilters : () => observableOf({})} }
],
schemas: [NO_ERRORS_SCHEMA]

View File

@@ -48,10 +48,7 @@ import { LogOutComponent } from './log-out/log-out.component';
import { FormComponent } from './form/form.component';
import { DsDynamicTypeaheadComponent } from './form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component';
import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
import {
DsDynamicFormControlContainerComponent,
dsDynamicFormControlMapFn
} from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
import { DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component';
import { DYNAMIC_FORM_CONTROL_MAP_FN, DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
@@ -174,9 +171,10 @@ import { PageWithSidebarComponent } from './sidebar/page-with-sidebar.component'
import { SidebarDropdownComponent } from './sidebar/sidebar-dropdown.component';
import { SidebarFilterComponent } from './sidebar/filter/sidebar-filter.component';
import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-filter-selected-option.component';
import { MetadataRepresentationListComponent } from '../+item-page/simple/metadata-representation-list/metadata-representation-list.component';
import { SelectableListItemControlComponent } from './object-collection/shared/selectable-list-item-control/selectable-list-item-control.component';
import { DsDynamicLookupRelationExternalSourceTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { ExistingMetadataListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -199,6 +197,7 @@ const MODULES = [
MomentModule,
TextMaskModule,
MenuModule,
DragDropModule
];
const ROOT_MODULES = [
@@ -335,7 +334,8 @@ const COMPONENTS = [
ItemSelectComponent,
CollectionSelectComponent,
MetadataRepresentationLoaderComponent,
SelectableListItemControlComponent
SelectableListItemControlComponent,
ExistingMetadataListElementComponent
];
const ENTRY_COMPONENTS = [
@@ -439,7 +439,8 @@ const DIRECTIVES = [
...DIRECTIVES,
...ENTRY_COMPONENTS,
...SHARED_ITEM_PAGE_COMPONENTS,
PublicationSearchResultListElementComponent
PublicationSearchResultListElementComponent,
ExistingMetadataListElementComponent
],
providers: [
...PROVIDERS

View File

@@ -10,6 +10,6 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
* Represents a single selected option in a sidebar filter
*/
export class SidebarFilterSelectedOptionComponent {
@Input() label:string;
@Output() click:EventEmitter<any> = new EventEmitter<any>();
@Input() label: string;
@Output() click: EventEmitter<any> = new EventEmitter<any>();
}

View File

@@ -45,7 +45,7 @@ export class FilterInitializeAction extends SidebarFilterAction {
type = SidebarFilterActionTypes.INITIALIZE;
initiallyExpanded;
constructor(name:string, initiallyExpanded:boolean) {
constructor(name: string, initiallyExpanded: boolean) {
super(name);
this.initiallyExpanded = initiallyExpanded;
}

View File

@@ -15,13 +15,13 @@ import { slide } from '../../animations/slide';
*/
export class SidebarFilterComponent implements OnInit {
@Input() name:string;
@Input() type:string;
@Input() label:string;
@Input() name: string;
@Input() type: string;
@Input() label: string;
@Input() expanded = true;
@Input() singleValue = false;
@Input() selectedValues:Observable<string[]>;
@Output() removeValue:EventEmitter<any> = new EventEmitter<any>();
@Input() selectedValues: Observable<string[]>;
@Output() removeValue: EventEmitter<any> = new EventEmitter<any>();
/**
* True when the filter is 100% collapsed in the UI
@@ -31,10 +31,10 @@ export class SidebarFilterComponent implements OnInit {
/**
* Emits true when the filter is currently collapsed in the store
*/
collapsed$:Observable<boolean>;
collapsed$: Observable<boolean>;
constructor(
protected filterService:SidebarFilterService,
protected filterService: SidebarFilterService,
) {
}
@@ -49,7 +49,7 @@ export class SidebarFilterComponent implements OnInit {
* Method to change this.collapsed to false when the slide animation ends and is sliding open
* @param event The animation event
*/
finishSlide(event:any):void {
finishSlide(event: any): void {
if (event.fromState === 'collapsed') {
this.closed = false;
}
@@ -59,13 +59,13 @@ export class SidebarFilterComponent implements OnInit {
* Method to change this.collapsed to true when the slide animation starts and is sliding closed
* @param event The animation event
*/
startSlide(event:any):void {
startSlide(event: any): void {
if (event.toState === 'collapsed') {
this.closed = true;
}
}
ngOnInit():void {
ngOnInit(): void {
this.closed = !this.expanded;
this.initializeFilter();
this.collapsed$ = this.isCollapsed();
@@ -82,7 +82,7 @@ export class SidebarFilterComponent implements OnInit {
* Checks if the filter is currently collapsed
* @returns {Observable<boolean>} Emits true when the current state of the filter is collapsed, false when it's expanded
*/
private isCollapsed():Observable<boolean> {
private isCollapsed(): Observable<boolean> {
return this.filterService.isCollapsed(this.name);
}

View File

@@ -8,17 +8,17 @@ import {
* Interface that represents the state for a single filters
*/
export interface SidebarFilterState {
filterCollapsed:boolean,
filterCollapsed: boolean,
}
/**
* Interface that represents the state for all available filters
*/
export interface SidebarFiltersState {
[name:string]:SidebarFilterState
[name: string]: SidebarFilterState
}
const initialState:SidebarFiltersState = Object.create(null);
const initialState: SidebarFiltersState = Object.create(null);
/**
* Performs a filter action on the current state
@@ -26,7 +26,7 @@ const initialState:SidebarFiltersState = Object.create(null);
* @param {SidebarFilterAction} action The action that should be performed
* @returns {SidebarFiltersState} The state after the action is performed
*/
export function sidebarFilterReducer(state = initialState, action:SidebarFilterAction):SidebarFiltersState {
export function sidebarFilterReducer(state = initialState, action: SidebarFilterAction): SidebarFiltersState {
switch (action.type) {

View File

@@ -16,7 +16,7 @@ import { hasValue } from '../../empty.util';
@Injectable()
export class SidebarFilterService {
constructor(private store:Store<SidebarFiltersState>) {
constructor(private store: Store<SidebarFiltersState>) {
}
/**
@@ -24,7 +24,7 @@ export class SidebarFilterService {
* @param {string} filter The filter for which the action is dispatched
* @param {boolean} expanded If the filter should be open from the start
*/
public initializeFilter(filter:string, expanded:boolean):void {
public initializeFilter(filter: string, expanded: boolean): void {
this.store.dispatch(new FilterInitializeAction(filter, expanded));
}
@@ -32,7 +32,7 @@ export class SidebarFilterService {
* Dispatches a collapse action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched
*/
public collapse(filterName:string):void {
public collapse(filterName: string): void {
this.store.dispatch(new FilterCollapseAction(filterName));
}
@@ -40,7 +40,7 @@ export class SidebarFilterService {
* Dispatches an expand action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched
*/
public expand(filterName:string):void {
public expand(filterName: string): void {
this.store.dispatch(new FilterExpandAction(filterName));
}
@@ -48,7 +48,7 @@ export class SidebarFilterService {
* Dispatches a toggle action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched
*/
public toggle(filterName:string):void {
public toggle(filterName: string): void {
this.store.dispatch(new FilterToggleAction(filterName));
}
@@ -57,10 +57,10 @@ export class SidebarFilterService {
* @param {string} filterName The filtername for which the collapsed state is checked
* @returns {Observable<boolean>} Emits the current collapsed state of the given filter, if it's unavailable, return false
*/
isCollapsed(filterName:string):Observable<boolean> {
isCollapsed(filterName: string): Observable<boolean> {
return this.store.pipe(
select(filterByNameSelector(filterName)),
map((object:SidebarFilterState) => {
map((object: SidebarFilterState) => {
if (object) {
return object.filterCollapsed;
} else {
@@ -73,14 +73,14 @@ export class SidebarFilterService {
}
const filterStateSelector = (state:SidebarFiltersState) => state.sidebarFilter;
const filterStateSelector = (state: SidebarFiltersState) => state.sidebarFilter;
function filterByNameSelector(name:string):MemoizedSelector<SidebarFiltersState, SidebarFilterState> {
function filterByNameSelector(name: string): MemoizedSelector<SidebarFiltersState, SidebarFilterState> {
return keySelector<SidebarFilterState>(name);
}
export function keySelector<T>(key:string):MemoizedSelector<SidebarFiltersState, T> {
return createSelector(filterStateSelector, (state:SidebarFilterState) => {
export function keySelector<T>(key: string): MemoizedSelector<SidebarFiltersState, T> {
return createSelector(filterStateSelector, (state: SidebarFilterState) => {
if (hasValue(state)) {
return state[key];
} else {

View File

@@ -7,8 +7,8 @@ import { HostWindowService } from '../host-window.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('PageWithSidebarComponent', () => {
let comp:PageWithSidebarComponent;
let fixture:ComponentFixture<PageWithSidebarComponent>;
let comp: PageWithSidebarComponent;
let fixture: ComponentFixture<PageWithSidebarComponent>;
const sidebarService = {
isCollapsed: observableOf(true),
@@ -42,7 +42,7 @@ describe('PageWithSidebarComponent', () => {
});
describe('when sidebarCollapsed is true in mobile view', () => {
let menu:HTMLElement;
let menu: HTMLElement;
beforeEach(() => {
menu = fixture.debugElement.query(By.css('#mock-id-sidebar-content')).nativeElement;
@@ -58,7 +58,7 @@ describe('PageWithSidebarComponent', () => {
});
describe('when sidebarCollapsed is false in mobile view', () => {
let menu:HTMLElement;
let menu: HTMLElement;
beforeEach(() => {
menu = fixture.debugElement.query(By.css('#mock-id-sidebar-content')).nativeElement;
@@ -70,6 +70,5 @@ describe('PageWithSidebarComponent', () => {
it('should open the menu', () => {
expect(menu.classList).toContain('active');
});
});
});

View File

@@ -18,13 +18,13 @@ import { map } from 'rxjs/operators';
* the template outlet (inside the page-width-sidebar tags).
*/
export class PageWithSidebarComponent implements OnInit {
@Input() id:string;
@Input() sidebarContent:TemplateRef<any>;
@Input() id: string;
@Input() sidebarContent: TemplateRef<any>;
/**
* Emits true if were on a small screen
*/
isXsOrSm$:Observable<boolean>;
isXsOrSm$: Observable<boolean>;
/**
* The width of the sidebar (bootstrap columns)
@@ -35,16 +35,16 @@ export class PageWithSidebarComponent implements OnInit {
/**
* Observable for whether or not the sidebar is currently collapsed
*/
isSidebarCollapsed$:Observable<boolean>;
isSidebarCollapsed$: Observable<boolean>;
sidebarClasses:Observable<string>;
sidebarClasses: Observable<string>;
constructor(protected sidebarService:SidebarService,
protected windowService:HostWindowService,
constructor(protected sidebarService: SidebarService,
protected windowService: HostWindowService,
) {
}
ngOnInit():void {
ngOnInit(): void {
this.isXsOrSm$ = this.windowService.isXsOrSm();
this.isSidebarCollapsed$ = this.isSidebarCollapsed();
this.sidebarClasses = this.isSidebarCollapsed$.pipe(
@@ -56,21 +56,21 @@ export class PageWithSidebarComponent implements OnInit {
* Check if the sidebar is collapsed
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
*/
private isSidebarCollapsed():Observable<boolean> {
private isSidebarCollapsed(): Observable<boolean> {
return this.sidebarService.isCollapsed;
}
/**
* Set the sidebar to a collapsed state
*/
public closeSidebar():void {
public closeSidebar(): void {
this.sidebarService.collapse()
}
/**
* Set the sidebar to an expanded state
*/
public openSidebar():void {
public openSidebar(): void {
this.sidebarService.expand();
}

View File

@@ -10,7 +10,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
* The options should still be provided in the content.
*/
export class SidebarDropdownComponent {
@Input() id:string;
@Input() label:string;
@Output() change:EventEmitter<any> = new EventEmitter<number>();
@Input() id: string;
@Input() label: string;
@Output() change: EventEmitter<any> = new EventEmitter<number>();
}

View File

@@ -10,7 +10,7 @@ export class ObjectKeysPipe implements PipeTransform {
* @param value An object
* @returns {any} Array with all keys the input object
*/
transform(value, args:string[]): any {
transform(value, args: string[]): any {
const keys = [];
Object.keys(value).forEach((k) => keys.push(k));
return keys;

View File

@@ -10,7 +10,7 @@ export class ObjectValuesPipe implements PipeTransform {
* @param value An object
* @returns {any} Array with all values of the input object
*/
transform(value, args:string[]): any {
transform(value, args: string[]): any {
const values = [];
Object.values(value).forEach((v) => values.push(v));
return values;

View File

@@ -5,9 +5,9 @@ import { filter } from 'rxjs/operators';
import { of as observableOf } from 'rxjs';
describe('Angulartics2DSpace', () => {
let provider:Angulartics2DSpace;
let angulartics2:Angulartics2;
let statisticsService:jasmine.SpyObj<StatisticsService>;
let provider: Angulartics2DSpace;
let angulartics2: Angulartics2;
let statisticsService: jasmine.SpyObj<StatisticsService>;
beforeEach(() => {
angulartics2 = {

View File

@@ -9,15 +9,15 @@ import { StatisticsService } from '../statistics.service';
export class Angulartics2DSpace {
constructor(
private angulartics2:Angulartics2,
private statisticsService:StatisticsService,
private angulartics2: Angulartics2,
private statisticsService: StatisticsService,
) {
}
/**
* Activates this plugin
*/
startTracking():void {
startTracking(): void {
this.angulartics2.eventTrack
.pipe(this.angulartics2.filterDeveloperMode())
.subscribe((event) => this.eventTrack(event));

View File

@@ -11,14 +11,14 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model';
templateUrl: './view-tracker.component.html',
})
export class ViewTrackerComponent implements OnInit {
@Input() object:DSpaceObject;
@Input() object: DSpaceObject;
constructor(
public angulartics2:Angulartics2
public angulartics2: Angulartics2
) {
}
ngOnInit():void {
ngOnInit(): void {
this.angulartics2.eventTrack.next({
action: 'pageView',
properties: {object: this.object},

View File

@@ -25,7 +25,7 @@ import { StatisticsService } from './statistics.service';
* This module handles the statistics
*/
export class StatisticsModule {
static forRoot():ModuleWithProviders {
static forRoot(): ModuleWithProviders {
return {
ngModule: StatisticsModule,
providers: [

View File

@@ -8,10 +8,10 @@ import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
import { SearchOptions } from '../shared/search/search-options.model';
describe('StatisticsService', () => {
let service:StatisticsService;
let requestService:jasmine.SpyObj<RequestService>;
let service: StatisticsService;
let requestService: jasmine.SpyObj<RequestService>;
const restURL = 'https://rest.api';
const halService:any = new HALEndpointServiceStub(restURL);
const halService: any = new HALEndpointServiceStub(restURL);
function initTestService() {
return new StatisticsService(
@@ -25,9 +25,9 @@ describe('StatisticsService', () => {
service = initTestService();
it('should send a request to track an item view ', () => {
const mockItem:any = {uuid: 'mock-item-uuid', type: 'item'};
const mockItem: any = {uuid: 'mock-item-uuid', type: 'item'};
service.trackViewEvent(mockItem);
const request:TrackRequest = requestService.configure.calls.mostRecent().args[0];
const request: TrackRequest = requestService.configure.calls.mostRecent().args[0];
expect(request.body).toBeDefined('request.body');
const body = JSON.parse(request.body);
expect(body.targetId).toBe('mock-item-uuid');
@@ -39,7 +39,7 @@ describe('StatisticsService', () => {
requestService = getMockRequestService();
service = initTestService();
const mockSearch:any = new SearchOptions({
const mockSearch: any = new SearchOptions({
query: 'mock-query',
});
@@ -51,7 +51,7 @@ describe('StatisticsService', () => {
};
const sort = {by: 'search-field', order: 'ASC'};
service.trackSearchEvent(mockSearch, page, sort);
const request:TrackRequest = requestService.configure.calls.mostRecent().args[0];
const request: TrackRequest = requestService.configure.calls.mostRecent().args[0];
const body = JSON.parse(request.body);
it('should specify the right query', () => {
@@ -79,7 +79,7 @@ describe('StatisticsService', () => {
requestService = getMockRequestService();
service = initTestService();
const mockSearch:any = new SearchOptions({
const mockSearch: any = new SearchOptions({
query: 'mock-query',
configuration: 'mock-configuration',
dsoType: DSpaceObjectType.ITEM,
@@ -108,7 +108,7 @@ describe('StatisticsService', () => {
}
];
service.trackSearchEvent(mockSearch, page, sort, filters);
const request:TrackRequest = requestService.configure.calls.mostRecent().args[0];
const request: TrackRequest = requestService.configure.calls.mostRecent().args[0];
const body = JSON.parse(request.body);
it('should specify the dsoType', () => {

View File

@@ -1,8 +1,28 @@
import { ChangeDetectorRef, Component, EventEmitter, HostListener, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import {
ChangeDetectorRef,
Component,
EventEmitter,
HostListener,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { BehaviorSubject, combineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, map, mergeMap, reduce, startWith, flatMap, find } from 'rxjs/operators';
import {
debounceTime,
distinctUntilChanged,
filter,
find,
flatMap,
map,
mergeMap,
reduce,
startWith
} from 'rxjs/operators';
import { Collection } from '../../../core/shared/collection.model';
import { CommunityDataService } from '../../../core/data/community-data.service';
@@ -227,8 +247,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
} else {
return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5);
}
})
);
}));
}
}
}

View File

@@ -113,6 +113,13 @@
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
],
"unified-signatures": true,