mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
created tabs, working on concat field lookups
This commit is contained in:
@@ -56,11 +56,10 @@ export class RelationshipTypeService {
|
||||
/* Flatten the page so we can treat it like an observable */
|
||||
switchMap((typeListRD: RemoteData<PaginatedList<RelationshipType>>) => typeListRD.payload.page),
|
||||
switchMap((type: RelationshipType) => {
|
||||
if (type.leftLabel === label) return this.checkType(type, firstType, secondType);
|
||||
else if (type.rightLabel === label) return this.checkType(type, secondType, firstType);
|
||||
if (type.rightLabel === label) return this.checkType(type, firstType, secondType);
|
||||
else if (type.leftLabel === label) return this.checkType(type, secondType, firstType);
|
||||
else return [];
|
||||
}),
|
||||
tap((t) => console.log(t))
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -6,5 +6,5 @@ export class MockResponseMap extends Map<string, any> {};
|
||||
export const MOCK_RESPONSE_MAP: InjectionToken<MockResponseMap> = new InjectionToken<MockResponseMap>('mockResponseMap');
|
||||
|
||||
export const mockResponseMap: MockResponseMap = new Map([
|
||||
[ '/config/submissionforms/traditionalpageone', mockSubmissionResponse ]
|
||||
// [ '/config/submissionforms/traditionalpageone', mockSubmissionResponse ]
|
||||
]);
|
||||
|
@@ -4,7 +4,8 @@ import {
|
||||
ComponentFactoryResolver,
|
||||
ContentChildren,
|
||||
EventEmitter,
|
||||
Input, NgZone,
|
||||
Input,
|
||||
NgZone,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
Output,
|
||||
@@ -69,7 +70,7 @@ 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 { map, switchMap, take } from 'rxjs/operators';
|
||||
import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer';
|
||||
import { Observable } from 'rxjs';
|
||||
import { SearchResult } from '../../../search/search-result.model';
|
||||
@@ -81,14 +82,7 @@ import { DsDynamicDisabledComponent } from './models/disabled/dynamic-disabled.c
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_DISABLED } from './models/disabled/dynamic-disabled.model';
|
||||
import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
||||
import { ItemViewMode } from '../../../items/item-type-decorator';
|
||||
import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model';
|
||||
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
|
||||
import { relationship } from '../../../../core/cache/builders/build-decorators';
|
||||
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||
import { RelationshipTypeService } from '../../../../core/data/relationship-type.service';
|
||||
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||
import { RelationshipOptions } from '../models/relationship-options.model';
|
||||
import { DsDynamicInputModel } from './models/ds-dynamic-input.model';
|
||||
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
|
@@ -2,6 +2,8 @@ import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelC
|
||||
import { isNotEmpty } from '../../../../empty.util';
|
||||
import { DsDynamicInputModel } from './ds-dynamic-input.model';
|
||||
import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model';
|
||||
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
|
||||
export const CONCAT_GROUP_SUFFIX = '_CONCAT_GROUP';
|
||||
export const CONCAT_FIRST_INPUT_SUFFIX = '_CONCAT_FIRST_INPUT';
|
||||
@@ -9,12 +11,19 @@ export const CONCAT_SECOND_INPUT_SUFFIX = '_CONCAT_SECOND_INPUT';
|
||||
|
||||
export interface DynamicConcatModelConfig extends DynamicFormGroupModelConfig {
|
||||
separator: string;
|
||||
value?: any;
|
||||
workspaceItem: WorkspaceItem;
|
||||
relationship?: RelationshipOptions;
|
||||
repeatable: boolean;
|
||||
}
|
||||
|
||||
export class DynamicConcatModel extends DynamicFormGroupModel {
|
||||
|
||||
@serializable() separator: string;
|
||||
@serializable() hasLanguages = false;
|
||||
@serializable() workspaceItem: WorkspaceItem;
|
||||
@serializable() relationship?: RelationshipOptions;
|
||||
@serializable() repeatable?: boolean;
|
||||
isCustomGroup = true;
|
||||
|
||||
constructor(config: DynamicConcatModelConfig, layout?: DynamicFormControlLayout) {
|
||||
@@ -22,6 +31,9 @@ export class DynamicConcatModel extends DynamicFormGroupModel {
|
||||
super(config, layout);
|
||||
|
||||
this.separator = config.separator + ' ';
|
||||
this.relationship = config.relationship;
|
||||
this.workspaceItem = config.workspaceItem;
|
||||
this.repeatable = config.repeatable;
|
||||
}
|
||||
|
||||
get value() {
|
||||
|
@@ -5,77 +5,22 @@
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngVar="(resultsRD$ | async) as resultsRD">
|
||||
<div class="row">
|
||||
<ds-search-sidebar class="col-4" id="search-sidebar"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||
[inPlaceSearch]="true"></ds-search-sidebar>
|
||||
<div class="col-8">
|
||||
<form class="input-group mb-3" #queryForm="ngForm"
|
||||
(ngSubmit)="search(queryForm.value.query)">
|
||||
<input type="text" class="form-control" name="query" [placeholder]="'submission.sections.describe.relationship-lookup.placeholder' | translate"
|
||||
[ngModel]="searchQuery">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="submit">{{ ('submission.sections.describe.relationship-lookup.search' | translate) }}</button>
|
||||
</div>
|
||||
</form>
|
||||
<div *ngIf="repeatable" class="position-absolute">
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<!-- In theory we don't need separate checkboxes for this,
|
||||
but I wasn't able to get this to work correctly without them.
|
||||
Checkboxes that are in the indeterminate state always switch to checked when clicked
|
||||
This seemed like the cleanest and clearest solution to solve this issue for now.
|
||||
-->
|
||||
<input *ngIf="!allSelected && !(someSelected$ | async)"
|
||||
type="checkbox"
|
||||
[indeterminate]="false"
|
||||
(change)="selectAll()">
|
||||
<input *ngIf="!allSelected && (someSelected$ | async)"
|
||||
type="checkbox"
|
||||
[indeterminate]="true"
|
||||
(change)="deselectAll()">
|
||||
<input *ngIf="allSelected" type="checkbox"
|
||||
[checked]="true"
|
||||
(change)="deselectAll()">
|
||||
</div>
|
||||
</div>
|
||||
<div ngbDropdown class="input-group-append">
|
||||
<button *ngIf="selectAllLoading" type="button"
|
||||
class="btn btn-outline-secondary rounded-right">
|
||||
<span class="spinner-border spinner-border-sm" role="status"
|
||||
aria-hidden="true"></span>
|
||||
<span class="sr-only">{{ ('submission.sections.describe.relationship-lookup.loading' | translate) }}</span>
|
||||
</button>
|
||||
<button id="resultdropdown" type="button"
|
||||
ngbDropdownToggle
|
||||
class="btn btn-outline-secondary dropdown-toggle-split"
|
||||
data-toggle="dropdown" aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
[hidden]="selectAllLoading">
|
||||
<span class="sr-only">{{ ('submission.sections.describe.relationship-lookup.toggle-dropdown' | translate) }}</span>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="resultdropdown">
|
||||
<button class="dropdown-item" (click)="selectPage(resultsRD?.payload?.page)">{{ ('submission.sections.describe.relationship-lookup.select-page' | translate) }}</button>
|
||||
<button class="dropdown-item" (click)="deselectPage(resultsRD?.payload?.page)">{{ ('submission.sections.describe.relationship-lookup.deselect-page' | translate) }}</button>
|
||||
<button class="dropdown-item" (click)="selectAll()">{{ ('submission.sections.describe.relationship-lookup.select-all' | translate) }}</button>
|
||||
<button class="dropdown-item" (click)="deselectAll()">{{ ('submission.sections.describe.relationship-lookup.deselect-all' | translate) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-search-results [searchResults]="resultsRD"
|
||||
[sortConfig]="this.searchConfig?.sort"
|
||||
[searchConfig]="this.searchConfig"
|
||||
[selectable]="true"
|
||||
[selectionConfig]="{ repeatable: repeatable, listId: listId }"
|
||||
(deselectObject)="deselect($event)"
|
||||
(selectObject)="select($event)"
|
||||
>
|
||||
</ds-search-results>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ngb-tabset>
|
||||
<ngb-tab title="Search">
|
||||
<ng-template ngbTabContent>
|
||||
<ds-dynamic-lookup-relation-search-tab
|
||||
[label]="label"
|
||||
[itemRD$]="itemRD$"
|
||||
[selection$]="selection$"
|
||||
[listId]="listId"
|
||||
[relationship]="relationship"
|
||||
[repeatable]="repeatable">
|
||||
</ds-dynamic-lookup-relation-search-tab>
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
</ngb-tabset>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<small>{{ ('submission.sections.describe.relationship-lookup.selected' | translate:{size: (selection$ | async)?.length || 0}) }}</small>
|
||||
|
@@ -1,11 +1,3 @@
|
||||
.result-list-element {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.position-absolute {
|
||||
right: $spacer;
|
||||
}
|
@@ -1,32 +1,14 @@
|
||||
import { Component, NgZone, OnDestroy, OnInit } from '@angular/core';
|
||||
import { PaginatedList } from '../../../../../core/data/paginated-list';
|
||||
import { SearchResult } from '../../../../search/search-result.model';
|
||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||
import { from, Observable, ReplaySubject } from 'rxjs';
|
||||
import { SearchService } from '../../../../../core/shared/search/search.service';
|
||||
import { PaginatedSearchOptions } from '../../../../search/paginated-search-options.model';
|
||||
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
|
||||
import { PaginationComponentOptions } from '../../../../pagination/pagination-component-options.model';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { hasValue, hasValueOperator, isNotEmpty } from '../../../../empty.util';
|
||||
import { concat, map, mergeMap, multicast, switchMap, take, takeWhile, tap } from 'rxjs/operators';
|
||||
import { Router } from '@angular/router';
|
||||
import { hasValue } from '../../../../empty.util';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../../+my-dspace-page/my-dspace-page.component';
|
||||
import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service';
|
||||
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
|
||||
import { SelectableListState } from '../../../../object-list/selectable-list/selectable-list.reducer';
|
||||
import { ListableObject } from '../../../../object-collection/shared/listable-object.model';
|
||||
import { RouteService } from '../../../../services/route.service';
|
||||
import { getSucceededRemoteData } from '../../../../../core/shared/operators';
|
||||
import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service';
|
||||
import { RelationshipType } from '../../../../../core/shared/item-relationships/relationship-type.model';
|
||||
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '../../../../../app.reducer';
|
||||
import { AddRelationshipAction, RemoveRelationshipAction } from './relationship.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-lookup-relation-modal',
|
||||
@@ -40,172 +22,26 @@ import { AddRelationshipAction, RemoveRelationshipAction } from './relationship.
|
||||
]
|
||||
})
|
||||
|
||||
export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy {
|
||||
export class DsDynamicLookupRelationModalComponent implements OnInit {
|
||||
label: string;
|
||||
relationship: RelationshipOptions;
|
||||
listId: string;
|
||||
resultsRD$: Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
|
||||
searchConfig: PaginatedSearchOptions;
|
||||
repeatable: boolean;
|
||||
searchQuery;
|
||||
allSelected: boolean;
|
||||
someSelected$: Observable<boolean>;
|
||||
selectAllLoading: boolean;
|
||||
subscription;
|
||||
initialPagination = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'submission-relation-list',
|
||||
pageSize: 5
|
||||
});
|
||||
selection$: Observable<ListableObject[]>;
|
||||
itemRD$;
|
||||
repeatable: boolean;
|
||||
selection$: Observable<ListableObject[]>;
|
||||
|
||||
constructor(
|
||||
public modal: NgbActiveModal,
|
||||
private searchService: SearchService,
|
||||
private router: Router,
|
||||
private selectableListService: SelectableListService,
|
||||
private searchConfigService: SearchConfigurationService,
|
||||
private routeService: RouteService,
|
||||
private relationshipService: RelationshipService,
|
||||
private relationshipTypeService: RelationshipTypeService,
|
||||
private zone: NgZone,
|
||||
private store: Store<AppState>
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.resetRoute();
|
||||
this.routeService.setParameter('fixedFilterQuery', this.relationship.filter);
|
||||
this.routeService.setParameter('configuration', this.relationship.searchConfiguration);
|
||||
|
||||
this.selection$ = this.selectableListService.getSelectableList(this.listId).pipe(map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : []));
|
||||
this.someSelected$ = this.selection$.pipe(map((selection) => isNotEmpty(selection)));
|
||||
this.resultsRD$ = this.searchConfigService.paginatedSearchOptions.pipe(
|
||||
map((options) => {
|
||||
return Object.assign(new PaginatedSearchOptions({}), options, { fixedFilter: this.relationship.filter, configuration: this.relationship.searchConfiguration })
|
||||
}),
|
||||
switchMap((options) => {
|
||||
this.searchConfig = options;
|
||||
return this.searchService.search(options).pipe(
|
||||
/* Make sure to only listen to the first x results, until loading is finished */
|
||||
/* TODO: in Rxjs 6.4.0 and up, we can replace this by takeWhile(predicate, true) - see https://stackoverflow.com/a/44644237 */
|
||||
multicast(
|
||||
() => new ReplaySubject(1),
|
||||
subject => subject.pipe(
|
||||
takeWhile((rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) => rd.isLoading),
|
||||
concat(subject.pipe(take(1))
|
||||
)
|
||||
)
|
||||
) as any
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
search(query: string) {
|
||||
this.allSelected = false;
|
||||
this.searchQuery = query;
|
||||
this.resetRoute();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
resetRoute() {
|
||||
this.router.navigate([], {
|
||||
queryParams: Object.assign({}, { page: 1, query: this.searchQuery, pageSize: this.initialPagination.pageSize }),
|
||||
});
|
||||
}
|
||||
|
||||
selectPage(page: SearchResult<Item>[]) {
|
||||
this.selection$
|
||||
.pipe(take(1))
|
||||
.subscribe((selection: SearchResult<Item>[]) => {
|
||||
const filteredPage = page.filter((pageItem) => selection.findIndex((selected) => selected.equals(pageItem)) < 0)
|
||||
this.select(...filteredPage);
|
||||
});
|
||||
this.selectableListService.select(this.listId, page);
|
||||
}
|
||||
|
||||
deselectPage(page: SearchResult<Item>[]) {
|
||||
this.allSelected = false;
|
||||
this.selection$
|
||||
.pipe(take(1))
|
||||
.subscribe((selection: SearchResult<Item>[]) => {
|
||||
const filteredPage = page.filter((pageItem) => selection.findIndex((selected) => selected.equals(pageItem)) >= 0)
|
||||
this.deselect(...filteredPage);
|
||||
});
|
||||
this.selectableListService.deselect(this.listId, page);
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
this.allSelected = true;
|
||||
this.selectAllLoading = true;
|
||||
const fullPagination = Object.assign(new PaginationComponentOptions(), {
|
||||
query: this.searchQuery,
|
||||
currentPage: 1,
|
||||
pageSize: 9999
|
||||
});
|
||||
const fullSearchConfig = Object.assign(this.searchConfig, { pagination: fullPagination });
|
||||
const results$ = this.searchService.search(fullSearchConfig) as Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
|
||||
results$.pipe(
|
||||
getSucceededRemoteData(),
|
||||
map((resultsRD) => resultsRD.payload.page),
|
||||
tap(() => this.selectAllLoading = false),
|
||||
).subscribe((results) => {
|
||||
this.selection$
|
||||
.pipe(take(1))
|
||||
.subscribe((selection: SearchResult<Item>[]) => {
|
||||
const filteredResults = results.filter((pageItem) => selection.findIndex((selected) => selected.equals(pageItem)) < 0);
|
||||
this.select(...filteredResults);
|
||||
});
|
||||
this.selectableListService.select(this.listId, results);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deselectAll() {
|
||||
this.allSelected = false;
|
||||
this.selection$
|
||||
.pipe(take(1))
|
||||
.subscribe((selection: SearchResult<Item>[]) => this.deselect(...selection));
|
||||
this.selectableListService.deselectAll(this.listId);
|
||||
}
|
||||
|
||||
|
||||
select(...selectableObjects: SearchResult<Item>[]) {
|
||||
this.zone.runOutsideAngular(
|
||||
() => this.itemRD$
|
||||
.pipe(
|
||||
getSucceededRemoteData(),
|
||||
tap((itemRD: RemoteData<Item>) => {
|
||||
return selectableObjects.forEach((object) =>
|
||||
this.store.dispatch(new AddRelationshipAction(itemRD.payload, object.indexableObject, this.relationship.relationshipType))
|
||||
);
|
||||
})
|
||||
).subscribe());
|
||||
}
|
||||
|
||||
|
||||
deselect(...selectableObjects: SearchResult<Item>[]) {
|
||||
this.zone.runOutsideAngular(
|
||||
() => this.itemRD$.pipe(
|
||||
getSucceededRemoteData(),
|
||||
tap((itemRD: RemoteData<Item>) => {
|
||||
return selectableObjects.forEach((object) =>
|
||||
this.store.dispatch(new RemoveRelationshipAction(itemRD.payload, object.indexableObject, this.relationship.relationshipType))
|
||||
);
|
||||
})
|
||||
).subscribe()
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.subscription)
|
||||
) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
@@ -69,21 +69,22 @@ export class RelationshipEffects {
|
||||
|
||||
|
||||
private createIdentifier(item1: Item, item2: Item, relationshipType: string): string {
|
||||
return `${item1.uuid}-${item2.uuid}-${relationshipType}$`;
|
||||
return `${item1.uuid}-${item2.uuid}-${relationshipType}`;
|
||||
}
|
||||
|
||||
|
||||
private addRelationship(item1: Item, item2: Item, relationshipType: string) {
|
||||
const type1: string = item1.firstMetadataValue('relationship.type');
|
||||
// const type1: string = item1.firstMetadataValue('relationship.type');
|
||||
const type1: string = 'JournalVolume';
|
||||
const type2: string = item2.firstMetadataValue('relationship.type');
|
||||
return this.relationshipTypeService.getRelationshipTypeByLabelAndTypes(relationshipType, type1, type2)
|
||||
.pipe(
|
||||
mergeMap((type: RelationshipType) => {
|
||||
const isSwitched = type.rightLabel === relationshipType;
|
||||
const isSwitched = type.leftLabel === relationshipType;
|
||||
if (isSwitched) {
|
||||
return this.relationshipService.addRelationship(type.id, cloneDeep(item2), cloneDeep(item1));
|
||||
return this.relationshipService.addRelationship(type.id, item2, item1);
|
||||
} else {
|
||||
return this.relationshipService.addRelationship(type.id, cloneDeep(item1), cloneDeep(item2));
|
||||
return this.relationshipService.addRelationship(type.id, item1, item2);
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@@ -0,0 +1,70 @@
|
||||
<div class="row" *ngVar="(resultsRD$ | async) as resultsRD">
|
||||
<ds-search-sidebar class="col-4" id="search-sidebar"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||
[inPlaceSearch]="true"></ds-search-sidebar>
|
||||
<div class="col-8">
|
||||
<form class="input-group mb-3" #queryForm="ngForm"
|
||||
(ngSubmit)="search(queryForm.value.query)">
|
||||
<input type="text" class="form-control" name="query" [placeholder]="'submission.sections.describe.relationship-lookup.placeholder' | translate"
|
||||
[ngModel]="searchQuery">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="submit">{{ ('submission.sections.describe.relationship-lookup.search' | translate) }}</button>
|
||||
</div>
|
||||
</form>
|
||||
<div *ngIf="repeatable" class="position-absolute">
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<!-- In theory we don't need separate checkboxes for this,
|
||||
but I wasn't able to get this to work correctly without them.
|
||||
Checkboxes that are in the indeterminate state always switch to checked when clicked
|
||||
This seemed like the cleanest and clearest solution to solve this issue for now.
|
||||
-->
|
||||
<input *ngIf="!allSelected && !(someSelected$ | async)"
|
||||
type="checkbox"
|
||||
[indeterminate]="false"
|
||||
(change)="selectAll()">
|
||||
<input *ngIf="!allSelected && (someSelected$ | async)"
|
||||
type="checkbox"
|
||||
[indeterminate]="true"
|
||||
(change)="deselectAll()">
|
||||
<input *ngIf="allSelected" type="checkbox"
|
||||
[checked]="true"
|
||||
(change)="deselectAll()">
|
||||
</div>
|
||||
</div>
|
||||
<div ngbDropdown class="input-group-append">
|
||||
<button *ngIf="selectAllLoading" type="button"
|
||||
class="btn btn-outline-secondary rounded-right">
|
||||
<span class="spinner-border spinner-border-sm" role="status"
|
||||
aria-hidden="true"></span>
|
||||
<span class="sr-only">{{ ('submission.sections.describe.relationship-lookup.loading' | translate) }}</span>
|
||||
</button>
|
||||
<button id="resultdropdown" type="button"
|
||||
ngbDropdownToggle
|
||||
class="btn btn-outline-secondary dropdown-toggle-split"
|
||||
data-toggle="dropdown" aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
[hidden]="selectAllLoading">
|
||||
<span class="sr-only">{{ ('submission.sections.describe.relationship-lookup.toggle-dropdown' | translate) }}</span>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="resultdropdown">
|
||||
<button class="dropdown-item" (click)="selectPage(resultsRD?.payload?.page)">{{ ('submission.sections.describe.relationship-lookup.select-page' | translate) }}</button>
|
||||
<button class="dropdown-item" (click)="deselectPage(resultsRD?.payload?.page)">{{ ('submission.sections.describe.relationship-lookup.deselect-page' | translate) }}</button>
|
||||
<button class="dropdown-item" (click)="selectAll()">{{ ('submission.sections.describe.relationship-lookup.select-all' | translate) }}</button>
|
||||
<button class="dropdown-item" (click)="deselectAll()">{{ ('submission.sections.describe.relationship-lookup.deselect-all' | translate) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-search-results [searchResults]="resultsRD"
|
||||
[sortConfig]="this.searchConfig?.sort"
|
||||
[searchConfig]="this.searchConfig"
|
||||
[selectable]="true"
|
||||
[selectionConfig]="{ repeatable: repeatable, listId: listId }"
|
||||
(deselectObject)="deselect($event)"
|
||||
(selectObject)="select($event)"
|
||||
>
|
||||
</ds-search-results>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,3 @@
|
||||
.position-absolute {
|
||||
right: $spacer;
|
||||
}
|
@@ -0,0 +1,204 @@
|
||||
import { Component, Input, NgZone, OnDestroy, OnInit } from '@angular/core';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../../../+my-dspace-page/my-dspace-page.component';
|
||||
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
|
||||
import { Item } from '../../../../../../core/shared/item.model';
|
||||
import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model';
|
||||
import { SearchResult } from '../../../../../search/search-result.model';
|
||||
import { PaginatedList } from '../../../../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../../../../core/data/remote-data';
|
||||
import { Observable, ReplaySubject } from 'rxjs';
|
||||
import { RelationshipOptions } from '../../../models/relationship-options.model';
|
||||
import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model';
|
||||
import { ListableObject } from '../../../../../object-collection/shared/listable-object.model';
|
||||
import { SearchService } from '../../../../../../core/shared/search/search.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service';
|
||||
import { RouteService } from '../../../../../services/route.service';
|
||||
import { RelationshipService } from '../../../../../../core/data/relationship.service';
|
||||
import { RelationshipTypeService } from '../../../../../../core/data/relationship-type.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '../../../../../../app.reducer';
|
||||
import { SelectableListState } from '../../../../../object-list/selectable-list/selectable-list.reducer';
|
||||
import { hasValue, isNotEmpty } from '../../../../../empty.util';
|
||||
import { concat, map, multicast, switchMap, take, takeWhile, tap } from 'rxjs/operators';
|
||||
import { DSpaceObject } from '../../../../../../core/shared/dspace-object.model';
|
||||
import { getSucceededRemoteData } from '../../../../../../core/shared/operators';
|
||||
import { AddRelationshipAction, RemoveRelationshipAction } from '../relationship.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-lookup-relation-search-tab',
|
||||
styleUrls: ['./dynamic-lookup-relation-search-tab.component.scss'],
|
||||
templateUrl: './dynamic-lookup-relation-search-tab.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDestroy {
|
||||
@Input() label: string;
|
||||
@Input() relationship: RelationshipOptions;
|
||||
@Input() listId: string;
|
||||
@Input() itemRD$;
|
||||
@Input() repeatable: boolean;
|
||||
@Input() selection$: Observable<ListableObject[]>;
|
||||
|
||||
resultsRD$: Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
|
||||
searchConfig: PaginatedSearchOptions;
|
||||
searchQuery;
|
||||
allSelected: boolean;
|
||||
someSelected$: Observable<boolean>;
|
||||
selectAllLoading: boolean;
|
||||
subscription;
|
||||
initialPagination = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'submission-relation-list',
|
||||
pageSize: 5
|
||||
});
|
||||
|
||||
constructor(
|
||||
private searchService: SearchService,
|
||||
private router: Router,
|
||||
private selectableListService: SelectableListService,
|
||||
private searchConfigService: SearchConfigurationService,
|
||||
private routeService: RouteService,
|
||||
private relationshipService: RelationshipService,
|
||||
private relationshipTypeService: RelationshipTypeService,
|
||||
private zone: NgZone,
|
||||
private store: Store<AppState>
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.resetRoute();
|
||||
this.routeService.setParameter('fixedFilterQuery', this.relationship.filter);
|
||||
this.routeService.setParameter('configuration', this.relationship.searchConfiguration);
|
||||
|
||||
this.selection$ = this.selectableListService.getSelectableList(this.listId).pipe(map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : []));
|
||||
this.someSelected$ = this.selection$.pipe(map((selection) => isNotEmpty(selection)));
|
||||
this.resultsRD$ = this.searchConfigService.paginatedSearchOptions.pipe(
|
||||
map((options) => {
|
||||
return Object.assign(new PaginatedSearchOptions({}), options, { fixedFilter: this.relationship.filter, configuration: this.relationship.searchConfiguration })
|
||||
}),
|
||||
switchMap((options) => {
|
||||
this.searchConfig = options;
|
||||
return this.searchService.search(options).pipe(
|
||||
/* Make sure to only listen to the first x results, until loading is finished */
|
||||
/* TODO: in Rxjs 6.4.0 and up, we can replace this with takeWhile(predicate, true) - see https://stackoverflow.com/a/44644237 */
|
||||
multicast(
|
||||
() => new ReplaySubject(1),
|
||||
subject => subject.pipe(
|
||||
takeWhile((rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) => rd.isLoading),
|
||||
concat(subject.pipe(take(1))
|
||||
)
|
||||
)
|
||||
) as any
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
search(query: string) {
|
||||
this.allSelected = false;
|
||||
this.searchQuery = query;
|
||||
this.resetRoute();
|
||||
}
|
||||
|
||||
resetRoute() {
|
||||
this.router.navigate([], {
|
||||
queryParams: Object.assign({}, { page: 1, query: this.searchQuery, pageSize: this.initialPagination.pageSize }),
|
||||
});
|
||||
}
|
||||
|
||||
selectPage(page: SearchResult<Item>[]) {
|
||||
this.selection$
|
||||
.pipe(take(1))
|
||||
.subscribe((selection: SearchResult<Item>[]) => {
|
||||
const filteredPage = page.filter((pageItem) => selection.findIndex((selected) => selected.equals(pageItem)) < 0)
|
||||
this.select(...filteredPage);
|
||||
});
|
||||
this.selectableListService.select(this.listId, page);
|
||||
}
|
||||
|
||||
deselectPage(page: SearchResult<Item>[]) {
|
||||
this.allSelected = false;
|
||||
this.selection$
|
||||
.pipe(take(1))
|
||||
.subscribe((selection: SearchResult<Item>[]) => {
|
||||
const filteredPage = page.filter((pageItem) => selection.findIndex((selected) => selected.equals(pageItem)) >= 0)
|
||||
this.deselect(...filteredPage);
|
||||
});
|
||||
this.selectableListService.deselect(this.listId, page);
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
this.allSelected = true;
|
||||
this.selectAllLoading = true;
|
||||
const fullPagination = Object.assign(new PaginationComponentOptions(), {
|
||||
query: this.searchQuery,
|
||||
currentPage: 1,
|
||||
pageSize: 9999
|
||||
});
|
||||
const fullSearchConfig = Object.assign(this.searchConfig, { pagination: fullPagination });
|
||||
const results$ = this.searchService.search(fullSearchConfig) as Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
|
||||
results$.pipe(
|
||||
getSucceededRemoteData(),
|
||||
map((resultsRD) => resultsRD.payload.page),
|
||||
tap(() => this.selectAllLoading = false),
|
||||
).subscribe((results) => {
|
||||
this.selection$
|
||||
.pipe(take(1))
|
||||
.subscribe((selection: SearchResult<Item>[]) => {
|
||||
const filteredResults = results.filter((pageItem) => selection.findIndex((selected) => selected.equals(pageItem)) < 0);
|
||||
this.select(...filteredResults);
|
||||
});
|
||||
this.selectableListService.select(this.listId, results);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deselectAll() {
|
||||
this.allSelected = false;
|
||||
this.selection$
|
||||
.pipe(take(1))
|
||||
.subscribe((selection: SearchResult<Item>[]) => this.deselect(...selection));
|
||||
this.selectableListService.deselectAll(this.listId);
|
||||
}
|
||||
|
||||
|
||||
select(...selectableObjects: SearchResult<Item>[]) {
|
||||
this.zone.runOutsideAngular(
|
||||
() => this.itemRD$
|
||||
.pipe(
|
||||
getSucceededRemoteData(),
|
||||
tap((itemRD: RemoteData<Item>) => {
|
||||
return selectableObjects.forEach((object) =>
|
||||
this.store.dispatch(new AddRelationshipAction(itemRD.payload, object.indexableObject, this.relationship.relationshipType))
|
||||
);
|
||||
})
|
||||
).subscribe());
|
||||
}
|
||||
|
||||
|
||||
deselect(...selectableObjects: SearchResult<Item>[]) {
|
||||
this.zone.runOutsideAngular(
|
||||
() => this.itemRD$.pipe(
|
||||
getSucceededRemoteData(),
|
||||
tap((itemRD: RemoteData<Item>) => {
|
||||
return selectableObjects.forEach((object) =>
|
||||
this.store.dispatch(new RemoveRelationshipAction(itemRD.payload, object.indexableObject, this.relationship.relationshipType))
|
||||
);
|
||||
})
|
||||
).subscribe()
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.subscription)
|
||||
) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
@@ -116,7 +116,6 @@ export class ObjectListComponent {
|
||||
if (value) {
|
||||
this.selectionService.selectSingle(this.selectionConfig.listId, object);
|
||||
this.selectObject.emit(object);
|
||||
|
||||
} else {
|
||||
this.selectionService.deselectSingle(this.selectionConfig.listId, object);
|
||||
this.deselectObject.emit(object);
|
||||
@@ -139,7 +138,5 @@ export class ObjectListComponent {
|
||||
this.selectObject.emit(object);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -160,6 +160,7 @@ import { SearchFacetRangeOptionComponent } from './search/search-filters/search-
|
||||
import { SearchSwitchConfigurationComponent } from './search/search-switch-configuration/search-switch-configuration.component';
|
||||
import { SearchAuthorityFilterComponent } from './search/search-filters/search-filter/search-authority-filter/search-authority-filter.component';
|
||||
import { DsDynamicDisabledComponent } from './form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component';
|
||||
import { DsDynamicLookupRelationSearchTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component';
|
||||
|
||||
const MODULES = [
|
||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||
@@ -360,7 +361,8 @@ const ENTRY_COMPONENTS = [
|
||||
SearchFacetOptionComponent,
|
||||
SearchFacetSelectedOptionComponent,
|
||||
SearchFacetRangeOptionComponent,
|
||||
SearchAuthorityFilterComponent
|
||||
SearchAuthorityFilterComponent,
|
||||
DsDynamicLookupRelationSearchTabComponent
|
||||
];
|
||||
|
||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||
|
Reference in New Issue
Block a user