progress july 11 - store/selection to object list

This commit is contained in:
lotte
2019-07-11 15:09:28 +02:00
parent 42c690dfd4
commit b1585ac7f2
14 changed files with 520 additions and 201 deletions

View File

@@ -23,6 +23,10 @@ import { hasValue } from './shared/empty.util';
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
import { historyReducer, HistoryState } from './shared/history/history.reducer';
import {
selectableListReducer,
SelectableListsState
} from './shared/object-list/selectable-list/selectable-list.reducer';
export interface AppState {
router: fromRouter.RouterReducerState;
@@ -36,6 +40,7 @@ export interface AppState {
truncatable: TruncatablesState;
cssVariables: CSSVariablesState;
menus: MenusState;
selectableLists: SelectableListsState
}
export const appReducers: ActionReducerMap<AppState> = {
@@ -50,6 +55,7 @@ export const appReducers: ActionReducerMap<AppState> = {
truncatable: truncatableReducer,
cssVariables: cssVariablesReducer,
menus: menusReducer,
selectableLists: selectableListReducer
};
export const routerStateSelector = (state: AppState) => state.router;

View File

@@ -100,6 +100,7 @@ import { SearchFilterService } from './shared/search/search-filter.service';
import { SearchFixedFilterService } from './shared/search/search-fixed-filter.service';
import { FilteredSearchPageGuard } from '../+search-page/filtered-search-page.guard';
import { SearchConfigurationService } from './shared/search/search-configuration.service';
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
export const restServiceFactory = (cfg: GlobalConfig, mocks: MockResponseMap, http: HttpClient) => {
// if (ENV_CONFIG.production) {
@@ -205,6 +206,7 @@ const PROVIDERS = [
FilteredSearchPageGuard,
SearchFilterService,
SearchConfigurationService,
SelectableListService,
// register AuthInterceptor as HttpInterceptor
{
provide: HTTP_INTERCEPTORS,

View File

@@ -5,111 +5,114 @@
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="search-page">
<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="Search query"
[ngModel]="searchQuery">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="submit">Go</button>
</div>
</form>
<ds-loading *ngIf="!resultsRD || resultsRD.isLoading"></ds-loading>
<div *ngIf="resultsRD?.hasSucceeded && resultsRD.payload.page.length > 0">
<div *ngIf="repeatable">
<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="!isAllSelected() && !isSomeSelected()"
type="checkbox"
[indeterminate]="false"
(change)="selectAll()">
<input *ngIf="!isAllSelected() && isSomeSelected()"
type="checkbox"
[indeterminate]="true"
(change)="deselectAll()">
<input *ngIf="isAllSelected()" type="checkbox"
[checked]="true"
(change)="deselectAll()">
</div>
</div>
<button *ngIf="selectAllLoading" type="button"
class="btn btn-outline-secondary">
<span class="spinner-border spinner-border-sm" role="status"
aria-hidden="true"></span>
<span class="sr-only">Loading...</span>
</button>
<div ngbDropdown class="input-group-append">
<button *ngIf="!selectAllLoading" id="resultdropdown" type="button"
ngbDropdownToggle
class="btn btn-outline-secondary dropdown-toggle-split"
data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<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="Search query"
[ngModel]="searchQuery">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="submit">Go</button>
</div>
</form>
<ds-search-results [searchResults]="resultsRD" [sortConfig]="this.searchConfig.sort"
[searchConfig]="this.searchConfig"
[selectable]="true"
[selectionConfig]="{ repeatable: repeatable, listId: listId }">
</ds-search-results>
<!--<ds-loading *ngIf="!resultsRD || resultsRD.isLoading"></ds-loading>-->
<!--<div *ngIf="resultsRD?.hasSucceeded && resultsRD.payload.page.length > 0">-->
<!--<div *ngIf="repeatable">-->
<!--<div class="input-group mb-3">-->
<!--<div class="input-group-prepend">-->
<!--<div class="input-group-text">-->
<!--&lt;!&ndash; 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.-->
<!--&ndash;&gt;-->
<!--<input *ngIf="!isAllSelected() && !isSomeSelected()"-->
<!--type="checkbox"-->
<!--[indeterminate]="false"-->
<!--(change)="selectAll()">-->
<!--<input *ngIf="!isAllSelected() && isSomeSelected()"-->
<!--type="checkbox"-->
<!--[indeterminate]="true"-->
<!--(change)="deselectAll()">-->
<!--<input *ngIf="isAllSelected()" type="checkbox"-->
<!--[checked]="true"-->
<!--(change)="deselectAll()">-->
<!--</div>-->
<!--</div>-->
<!--<button *ngIf="selectAllLoading" type="button"-->
<!--class="btn btn-outline-secondary">-->
<!--<span class="spinner-border spinner-border-sm" role="status"-->
<!--aria-hidden="true"></span>-->
<!--<span class="sr-only">Loading...</span>-->
<!--</button>-->
<!--<div ngbDropdown class="input-group-append">-->
<!--<button *ngIf="!selectAllLoading" id="resultdropdown" type="button"-->
<!--ngbDropdownToggle-->
<!--class="btn btn-outline-secondary dropdown-toggle-split"-->
<!--data-toggle="dropdown" aria-haspopup="true"-->
<!--aria-expanded="false">-->
<!--<span class="sr-only">Toggle Dropdown</span>-->
<!--</button>-->
<div ngbDropdownMenu aria-labelledby="resultdropdown">
<button class="dropdown-item"
(click)="selectPage(resultsRD?.payload?.page)">Select
page
</button>
<button class="dropdown-item"
(click)="deselectPage(resultsRD?.payload?.page)">
Deselect
page
</button>
<button class="dropdown-item" (click)="selectAll()">Select all
</button>
<button class="dropdown-item" (click)="deselectAll()">Deselect
all
</button>
</div>
</div>
</div>
</div>
<ds-pagination
[paginationOptions]="searchConfig.pagination"
[collectionSize]="resultsRD?.payload?.totalElements"
[sortOptions]="searchConfig.sort"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(paginationChange)="onPaginationChange($event.pagination)">
<div class="form-check"
*ngFor="let result of resultsRD?.payload?.page; let i = index">
<input *ngIf="repeatable" class="form-check-input" type="checkbox"
[name]="'checkbox' + i"
[id]="'object'+i"
[checked]="isSelected(result.indexableObject)"
[disabled]="isDisabled(result.indexableObject)"
(change)="selectCheckbox($event.currentTarget.checked, result.indexableObject)">
<input *ngIf="!repeatable" class="form-check-input" type="radio"
[name]="'radio' + i"
[id]="'object'+i"
[checked]="isSelected(result.indexableObject)"
(change)="selectRadio($event.currentTarget.checked, result.indexableObject)">
<label class="form-check-label" [for]="'object'+i">
<ds-wrapper-list-element class="result-list-element"
[object]="result"
[index]="i"></ds-wrapper-list-element>
</label>
</div>
</ds-pagination>
</div>
<div *ngIf="resultsRD?.hasSucceeded && resultsRD.payload.page.length === 0">
{{ 'form.no-results' | translate}}
</div>
</div>
<!--<div ngbDropdownMenu aria-labelledby="resultdropdown">-->
<!--<button class="dropdown-item"-->
<!--(click)="selectPage(resultsRD?.payload?.page)">Select-->
<!--page-->
<!--</button>-->
<!--<button class="dropdown-item"-->
<!--(click)="deselectPage(resultsRD?.payload?.page)">-->
<!--Deselect-->
<!--page-->
<!--</button>-->
<!--<button class="dropdown-item" (click)="selectAll()">Select all-->
<!--</button>-->
<!--<button class="dropdown-item" (click)="deselectAll()">Deselect-->
<!--all-->
<!--</button>-->
<!--</div>-->
<!--</div>-->
<!--</div>-->
<!--</div>-->
<!--<ds-pagination-->
<!--[paginationOptions]="searchConfig.pagination"-->
<!--[collectionSize]="resultsRD?.payload?.totalElements"-->
<!--[sortOptions]="searchConfig.sort"-->
<!--[hideGear]="true"-->
<!--[hidePagerWhenSinglePage]="true"-->
<!--(paginationChange)="onPaginationChange($event.pagination)">-->
<!--<div class="form-check"-->
<!--*ngFor="let result of resultsRD?.payload?.page; let i = index">-->
<!--<input *ngIf="repeatable" class="form-check-input" type="checkbox"-->
<!--[name]="'checkbox' + i"-->
<!--[id]="'object'+i"-->
<!--[checked]="isSelected(result.indexableObject)"-->
<!--[disabled]="isDisabled(result.indexableObject)"-->
<!--(change)="selectCheckbox($event.currentTarget.checked, result.indexableObject)">-->
<!--<input *ngIf="!repeatable" class="form-check-input" type="radio"-->
<!--[name]="'radio' + i"-->
<!--[id]="'object'+i"-->
<!--[checked]="isSelected(result.indexableObject)"-->
<!--(change)="selectRadio($event.currentTarget.checked, result.indexableObject)">-->
<!--<label class="form-check-label" [for]="'object'+i">-->
<!--<ds-wrapper-list-element class="result-list-element"-->
<!--[object]="result"-->
<!--[index]="i"></ds-wrapper-list-element>-->
<!--</label>-->
<!--</div>-->
<!--</ds-pagination>-->
<!--</div>-->
<!--<div *ngIf="resultsRD?.hasSucceeded && resultsRD.payload.page.length === 0">-->
<!--{{ 'form.no-results' | translate}}-->
<!--</div>-->
</div>
</div>
</div>

View File

@@ -14,6 +14,7 @@ import { concat, map, multicast, take, takeWhile, tap } from 'rxjs/operators';
import { Router } from '@angular/router';
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';
const RELATION_TYPE_FILTER_PREFIX = 'f.entityType=';
@@ -37,22 +38,21 @@ export class DsDynamicLookupRelationModalComponent implements OnInit {
resultsRD$: Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>;
searchConfig: PaginatedSearchOptions;
repeatable: boolean;
selection: DSpaceObject[] = [];
previousSelection: DSpaceObject[] = [];
allSelected = false;
searchQuery;
initialPagination = Object.assign(new PaginationComponentOptions(), {
id: 'submission-relation-list',
pageSize: 10
});
selectAllLoading = false;
listId;
constructor(public modal: NgbActiveModal, private searchService: SearchService, private router: Router) {
constructor(public modal: NgbActiveModal, private searchService: SearchService, private router: Router, private selectableListService: SelectableListService) {
}
ngOnInit(): void {
this.resetRoute();
this.fieldName = this.relationKey.substring(RELATION_TYPE_METADATA_PREFIX.length);
this.listId = 'list-' + this.fieldName;
this.onPaginationChange(this.initialPagination);
}
@@ -60,7 +60,7 @@ export class DsDynamicLookupRelationModalComponent implements OnInit {
this.searchQuery = query;
this.resetRoute();
this.onPaginationChange(this.initialPagination);
this.deselectAll();
this.selectableListService.deselectAll(this.listId);
}
onPaginationChange(pagination: PaginationComponentOptions) {
@@ -84,84 +84,7 @@ export class DsDynamicLookupRelationModalComponent implements OnInit {
}
close() {
this.modal.close(this.selection);
}
isSelected(dso: DSpaceObject): boolean {
const completeSelection = [...this.selection, ...this.previousSelection];
return hasValue(completeSelection.find((selected) => selected.uuid === dso.uuid));
}
isDisabled(dso: DSpaceObject): boolean {
return hasValue(this.previousSelection.find((selected) => selected.uuid === dso.uuid));
}
selectCheckbox(value: boolean, dso: DSpaceObject) {
if (value) {
this.selection = [...this.selection, dso];
} else {
this.allSelected = false;
this.selection = this.selection.filter((selected) => {
return selected.uuid !== dso.uuid
});
}
}
selectRadio(value: boolean, dso: DSpaceObject) {
if (value) {
this.selection = [dso];
}
}
selectPage(page: SearchResult<DSpaceObject>[]) {
const newObjects: DSpaceObject[] = page
.map((searchResult) => searchResult.indexableObject)
.filter((dso) => hasNoValue(this.selection.find((selected) => selected.uuid === dso.uuid)))
.filter((dso) => hasNoValue(this.previousSelection.find((object) => object.uuid === dso.uuid)));
this.selection = [...this.selection, ...newObjects]
}
deselectPage(page: SearchResult<DSpaceObject>[]) {
this.allSelected = false;
const objects: DSpaceObject[] = page
.map((searchResult) => searchResult.indexableObject);
this.selection = this.selection.filter((selected) => hasNoValue(objects.find((object) => object.uuid === selected.uuid)));
}
selectAll() {
this.allSelected = true;
this.selectAllLoading = true;
const fullPagination = Object.assign(new PaginationComponentOptions(), {
query: this.searchQuery,
currentPage: 1,
pageSize: Number.POSITIVE_INFINITY
});
const fullSearchConfig = Object.assign(this.searchConfig, { pagination: fullPagination });
const results = this.searchService.search(fullSearchConfig);
results.pipe(
getSucceededRemoteData(),
map((resultsRD) => resultsRD.payload.page),
tap(() => this.selectAllLoading = false)
)
.subscribe((results) =>
this.selection = results
.map((searchResult) => searchResult.indexableObject)
.filter((dso) => hasNoValue(this.previousSelection.find((object) => object.uuid === dso.uuid)))
);
}
deselectAll() {
this.allSelected = false;
this.selection = [];
}
isAllSelected() {
return this.allSelected;
}
isSomeSelected() {
return isNotEmpty(this.selection);
this.modal.close(this.selectableListService.getSelectableList(this.listId));
}
resetRoute() {

View File

@@ -8,6 +8,8 @@
(pageSizeChange)="onPageSizeChange($event)"
(sortDirectionChange)="onSortDirectionChange($event)"
(sortFieldChange)="onSortFieldChange($event)"
[selectable]="selectable"
[selectionConfig]="selectionConfig"
*ngIf="getViewMode()===viewModeEnum.List">
</ds-object-list>

View File

@@ -33,6 +33,9 @@ export class ObjectCollectionComponent implements OnChanges, OnInit {
@Input() sortConfig: SortOptions;
@Input() hasBorder = false;
@Input() hideGear = false;
@Input() selectable = false;
@Input() selectionConfig: {repeatable: boolean, listId: string};
pageInfo: Observable<PageInfo>;
private sub;
/**

View File

@@ -12,6 +12,16 @@
(paginationChange)="onPaginationChange($event)">
<ul *ngIf="objects?.hasSucceeded" class="list-unstyled">
<li *ngFor="let object of objects?.payload?.page; let i = index; let last = last" class="mt-4 mb-4" [class.border-bottom]="hasBorder && !last">
<input *ngIf="selectable && selectionConfig.repeatable" class="form-check-input" type="checkbox"
[name]="'checkbox' + i"
[id]="'object'+i"
[checked]="selectionService.isObjectSelected(selectionConfig.listId, object) | async"
(change)="selectCheckbox($event.currentTarget.checked, object)">
<input *ngIf="selectable && !selectionConfig.repeatable" class="form-check-input" type="radio"
[name]="'radio' + i"
[id]="'object'+i"
[checked]="selectionService.isObjectSelected(selectionConfig.listId, object) | async"
(change)="selectRadio($event.currentTarget.checked, object)">
<ds-wrapper-list-element [object]="object" [index]="i"></ds-wrapper-list-element>
</li>
</ul>

View File

@@ -12,6 +12,9 @@ import { RemoteData } from '../../core/data/remote-data';
import { fadeIn } from '../animations/fade';
import { ListableObject } from '../object-collection/shared/listable-object.model';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { SearchResult } from '../search/search-result.model';
import { SelectableListService } from './selectable-list/selectable-list.service';
@Component({
changeDetection: ChangeDetectionStrategy.Default,
@@ -22,13 +25,22 @@ import { PaginationComponentOptions } from '../pagination/pagination-component-o
animations: [fadeIn]
})
export class ObjectListComponent {
@Input() config: PaginationComponentOptions;
@Input() sortConfig: SortOptions;
@Input() hasBorder = false;
@Input() hideGear = false;
@Input() hidePagerWhenSinglePage = true;
@Input() selectable = false;
@Input() selectionConfig: { repeatable: boolean, listId: string };
// @Input() previousSelection: ListableObject[] = [];
// allSelected = false;
// selectAllLoading = false;
private _objects: RemoteData<PaginatedList<ListableObject>>;
constructor(protected selectionService: SelectableListService) {
}
@Input() set objects(objects: RemoteData<PaginatedList<ListableObject>>) {
this._objects = objects;
}
@@ -96,4 +108,66 @@ export class ObjectListComponent {
this.paginationChange.emit(event);
}
// isDisabled(object: ListableObject): boolean {
// return hasValue(this.previousSelection.find((selected) => selected === object));
// }
selectCheckbox(value: boolean, object: ListableObject) {
if (value) {
this.selectionService.selectSingle(this.selectionConfig.listId, object);
} else {
this.selectionService.deselectSingle(this.selectionConfig.listId, object);
}
}
selectRadio(value: boolean, object: ListableObject) {
if (value) {
this.selectionService.selectSingle(this.selectionConfig.listId, object, false);
}
}
selectPage(page: SearchResult<DSpaceObject>[]) {
this.selectionService.select(this.selectionConfig.listId, this.objects.payload.page);
}
deselectPage(page: SearchResult<DSpaceObject>[]) {
this.selectionService.deselect(this.selectionConfig.listId, this.objects.payload.page);
}
deselectAll() {
this.selectionService.deselectAll(this.selectionConfig.listId);
}
// isAllSelected() {
// return this.allSelected;
// }
//
// isSomeSelected() {
// return isNotEmpty(this.selection);
// }
//
//
// selectAll() {
// this.allSelected = true;
// this.selectAllLoading = true;
// const fullPagination = Object.assign(new PaginationComponentOptions(), {
// query: this.searchQuery,
// currentPage: 1,
// pageSize: Number.POSITIVE_INFINITY
// });
// const fullSearchConfig = Object.assign(this.searchConfig, { pagination: fullPagination });
// const results = this.searchService.search(fullSearchConfig);
// results.pipe(
// getSucceededRemoteData(),
// map((resultsRD) => resultsRD.payload.page),
// tap(() => this.selectAllLoading = false)
// )
// .subscribe((results) =>
// this.selection = results
// .map((searchResult) => searchResult.indexableObject)
// .filter((dso) => hasNoValue(this.previousSelection.find((object) => object === dso)))
// );
// }
}

View File

@@ -0,0 +1,87 @@
import { Action } from '@ngrx/store';
import { type } from '../../ngrx/type';
import { ListableObject } from '../../object-collection/shared/listable-object.model';
/**
* For each action type in an action group, make a simple
* enum object for all of this group's action types.
*
* The 'type' utility function coerces strings into string
* literal types and runs a simple check to guarantee all
* action types in the application are unique.
*/
export const SelectableListActionTypes = {
SELECT: type('dspace/selectable-lists/SELECT'),
SELECT_SINGLE: type('dspace/selectable-lists/SELECT_SINGLE'),
DESELECT: type('dspace/selectable-lists/DESELECT'),
DESELECT_SINGLE: type('dspace/selectable-lists/DESELECT_SINGLE'),
SET_SELECTION: type('dspace/selectable-lists/SET_SELECTION'),
DESELECT_ALL: type('dspace/selectable-lists/DESELECT_ALL')
};
/* tslint:disable:max-classes-per-file */
export abstract class SelectableListAction implements Action {
constructor(public type, public id: string) {
}
}
/**
* Used to select an item in a the selectable list
*/
export class SelectableListSelectAction extends SelectableListAction {
payload: ListableObject[];
constructor(id: string, objects: ListableObject[]) {
super(SelectableListActionTypes.SELECT_SINGLE, id);
this.payload = objects;
}
}
export class SelectableListSelectSingleAction extends SelectableListAction {
payload: {
object: ListableObject,
multipleSelectionsAllowed: boolean
};
constructor(id: string, object: ListableObject, multipleSelectionsAllowed: boolean = true) {
super(SelectableListActionTypes.SELECT, id);
this.payload = { object, multipleSelectionsAllowed };
}
}
export class SelectableListDeselectSingleAction extends SelectableListAction {
payload: ListableObject;
constructor(id: string, object: ListableObject) {
super(SelectableListActionTypes.DESELECT_SINGLE, id);
this.payload = object;
}
}
export class SelectableListDeselectAction extends SelectableListAction {
payload: ListableObject[];
constructor(id: string, objects: ListableObject[]) {
super(SelectableListActionTypes.DESELECT, id);
this.payload = objects;
}
}
export class SelectableListSetSelectionAction extends SelectableListAction {
payload: ListableObject;
constructor(id: string, objects: ListableObject[]) {
super(SelectableListActionTypes.SET_SELECTION, id);
this.payload = objects;
}
}
export class SelectableListDeselectAllAction extends SelectableListAction {
constructor(id: string) {
super(SelectableListActionTypes.DESELECT_ALL, id);
}
}
/* tslint:enable:max-classes-per-file */

View File

@@ -0,0 +1,106 @@
import { ListableObject } from '../../object-collection/shared/listable-object.model';
import {
SelectableListAction,
SelectableListActionTypes,
SelectableListSelectAction,
SelectableListSelectSingleAction,
SelectableListDeselectAction,
SelectableListDeselectSingleAction, SelectableListSetSelectionAction
} from './selectable-list.actions';
import { hasNoValue } from '../../empty.util';
/**
* Represents the state of all selectable lists in the store
*/
export type SelectableListsState = {
[id: string]: SelectableListState;
}
/**
* Represents the state of a single selectable list in the store
*/
export interface SelectableListState {
id: string;
selection: ListableObject[];
}
/**
* Reducer that handles SelectableListAction to update the SelectableListsState
* @param {SelectableListsState} state The initial SelectableListsState
* @param {SelectableListAction} action The Action to be performed on the state
* @returns {SelectableListsState} The new, reducer SelectableListsState
*/
export function selectableListReducer(state: SelectableListsState = {}, action: SelectableListAction): SelectableListsState {
const listState: SelectableListState = state[action.id] || clearSelection(action.id);
switch (action.type) {
case SelectableListActionTypes.SELECT: {
const newListState = select(listState, action as SelectableListSelectAction);
return Object.assign({}, state, { [action.id]: newListState });
}
case SelectableListActionTypes.SELECT_SINGLE: {
const newListState = selectSingle(listState, action as SelectableListSelectSingleAction);
return Object.assign({}, state, { [action.id]: newListState });
}
case SelectableListActionTypes.DESELECT: {
const newListState = deselect(listState, action as SelectableListDeselectAction);
return Object.assign({}, state, { [action.id]: newListState });
}
case SelectableListActionTypes.DESELECT_SINGLE: {
const newListState = deselectSingle(listState, action as SelectableListDeselectSingleAction);
return Object.assign({}, state, { [action.id]: newListState });
}
case SelectableListActionTypes.SET_SELECTION: {
const newListState = setList(listState, action as SelectableListSetSelectionAction);
return Object.assign({}, state, { [action.id]: newListState });
}
case SelectableListActionTypes.DESELECT_ALL: {
const newListState = clearSelection(action.id);
return Object.assign({}, state, { [action.id]: newListState });
}
default: {
return state;
}
}
}
function select(state: SelectableListState, action: SelectableListSelectAction) {
const filteredNewObjects = action.payload.filter((object) => !isObjectInSelection(state.selection, object));
const newSelection = [...state.selection, ...filteredNewObjects];
return Object.assign({}, state, { selection: newSelection });
}
function selectSingle(state: SelectableListState, action: SelectableListSelectSingleAction) {
let newSelection;
if (action.payload.multipleSelectionsAllowed && !isObjectInSelection(state.selection, action.payload)) {
newSelection = [...state.selection, action.payload.object];
} else {
newSelection = [action.payload.object];
}
return Object.assign({}, state, { selection: newSelection });
}
function deselect(state: SelectableListState, action: SelectableListDeselectAction) {
const newSelection = state.selection.filter((selected) => hasNoValue(action.payload.find((object) => object === selected)));
return Object.assign({}, state, { selection: newSelection });
}
function deselectSingle(state: SelectableListState, action: SelectableListDeselectSingleAction) {
const newSelection = state.selection.filter((selected) => {
return selected !== action.payload
});
return Object.assign({}, state, { selection: newSelection });
}
function setList(state: SelectableListState, action: SelectableListSetSelectionAction) {
const newSelection = [...state.selection, action.payload];
return Object.assign({}, state, { selection: newSelection });
}
function clearSelection(id: string) {
return { id: id, selection: [] };
}
function isObjectInSelection(selection: ListableObject[], object: ListableObject) {
return selection.findIndex((selected) => selected === object) >= 0
}

View File

@@ -0,0 +1,94 @@
import { Injectable } from '@angular/core';
import { MemoizedSelector, select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, startWith, tap } from 'rxjs/operators';
import { SelectableListsState, SelectableListState } from './selectable-list.reducer';
import { AppState, keySelector } from '../../../app.reducer';
import { ListableObject } from '../../object-collection/shared/listable-object.model';
import {
SelectableListDeselectAction, SelectableListDeselectAllAction,
SelectableListDeselectSingleAction, SelectableListSelectAction,
SelectableListSelectSingleAction
} from './selectable-list.actions';
import { hasNoValue, hasValue, isNotEmpty } from '../../empty.util';
const selectableListsStateSelector = (state) => state.selectableLists;
const menuByIDSelector = (id: string): MemoizedSelector<AppState, SelectableListState> => {
return keySelector<SelectableListState>(id, selectableListsStateSelector);
};
@Injectable()
export class SelectableListService {
constructor(private store: Store<SelectableListsState>) {
}
/**
* Retrieve a selectable list's state by its ID
* @param {string} id ID of the requested Selectable list
* @returns {Observable<SelectableListState>} Observable that emits the current state of the requested selectable list
*/
getSelectableList(id: string): Observable<SelectableListState> {
return this.store.pipe(select(menuByIDSelector(id)));
}
/**
* Select an object in a specific list in the store
* @param {string} id The id of the list on which the object should be selected
* @param {ListableObject} object The object to select
*/
selectSingle(id: string, object: ListableObject, multipleSelectionsAllowed?) {
this.store.dispatch(new SelectableListSelectSingleAction(id, object, multipleSelectionsAllowed));
}
/**
* Select multiple objects in a specific list in the store
* @param {string} id The id of the list on which the objects should be selected
* @param {ListableObject[]} objects The objects to select
*/
select(id: string, objects: ListableObject[]) {
this.store.dispatch(new SelectableListSelectAction(id, objects));
}
/**
* Deselect an object in a specific list in the store
* @param {string} id The id of the list on which the object should be deselected
* @param {ListableObject} object The object to deselect
*/
deselectSingle(id: string, object: ListableObject) {
this.store.dispatch(new SelectableListDeselectSingleAction(id, object));
}
/**
* Deselect multiple objects in a specific list in the store
* @param {string} id The id of the list on which the objects should be deselected
* @param {ListableObject[]} objects The objects to deselect
*/
deselect(id: string, objects: ListableObject[]) {
this.store.dispatch(new SelectableListDeselectAction(id, objects));
}
/**
* Deselect all objects in a specific list in the store
* @param {string} id The id of the list on which the objects should be deselected
*/
deselectAll(id: string) {
this.store.dispatch(new SelectableListDeselectAllAction(id));
}
/**
* Check if a given object is selected in a specific list
* @param {string} id The ID of the selectable list the object should be selected in
* @param {ListableObject} object The object to check for if it's selected
* @returns {Observable<boolean>} Emits true if the given object is selected, emits false when it's deselected
*/
isObjectSelected(id: string, object: ListableObject): Observable<boolean> {
return this.getSelectableList(id).pipe(
filter((state: SelectableListState) => hasValue(state)),
map((state: SelectableListState) => isNotEmpty(state.selection) && hasValue(state.selection.find((selected) => selected === object))),
startWith(false),
distinctUntilChanged()
);
}
}

View File

@@ -51,7 +51,6 @@ export class SearchFiltersComponent implements OnInit {
}
ngOnInit(): void {
this.filters = this.searchConfigService.searchOptions.pipe(
switchMap((options) => this.searchService.getConfig(options.scope, options.configuration).pipe(getSucceededRemoteData()))
);
@@ -78,5 +77,4 @@ export class SearchFiltersComponent implements OnInit {
trackUpdate(index, config: SearchFilterConfig) {
return config ? config.name : undefined;
}
}

View File

@@ -1,18 +1,26 @@
<h2 *ngIf="!disableHeader">{{ getTitleKey() | translate }}</h2>
<div *ngIf="searchResults?.hasSucceeded && !searchResults?.isLoading && searchResults?.payload?.page.length > 0" @fadeIn>
<ds-viewable-collection
[config]="searchConfig.pagination"
[sortConfig]="searchConfig.sort"
[objects]="searchResults"
[hideGear]="true">
</ds-viewable-collection></div>
<ds-loading *ngIf="hasNoValue(searchResults) || hasNoValue(searchResults.payload) || searchResults.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading>
<ds-error *ngIf="searchResults?.hasFailed && (!searchResults?.error || searchResults?.error?.statusCode != 400)" message="{{'error.search-results' | translate}}"></ds-error>
<ds-viewable-collection
[config]="searchConfig.pagination"
[sortConfig]="searchConfig.sort"
[objects]="searchResults"
[hideGear]="true"
[selectable]="selectable"
[selectionConfig]="selectionConfig"
>
</ds-viewable-collection>
</div>
<ds-loading
*ngIf="hasNoValue(searchResults) || hasNoValue(searchResults.payload) || searchResults.isLoading"
message="{{'loading.search-results' | translate}}"></ds-loading>
<ds-error
*ngIf="searchResults?.hasFailed && (!searchResults?.error || searchResults?.error?.statusCode != 400)"
message="{{'error.search-results' | translate}}"></ds-error>
<div *ngIf="searchResults?.payload?.page.length == 0 || searchResults?.error?.statusCode == 400">
{{ 'search.results.no-results' | translate }}
<a [routerLink]="['/search']"
[queryParams]="{ query: surroundStringWithQuotes(searchConfig?.query) }"
queryParamsHandling="merge">
{{"search.results.no-results-link" | translate}}
[queryParams]="{ query: surroundStringWithQuotes(searchConfig?.query) }"
queryParamsHandling="merge">
{{"search.results.no-results-link" | translate}}
</a>
</div>

View File

@@ -55,6 +55,9 @@ export class SearchResultsComponent {
*/
@Input() disableHeader = false;
@Input() selectable = false;
@Input() selectionConfig: {repeatable: boolean, listId: string};
/**
* Get the i18n key for the title depending on the fixed filter
* Defaults to 'search.results.head' if there's no fixed filter found