44024: simple search UI with decorator bug

This commit is contained in:
Lotte Hofstede
2017-08-18 14:23:06 +02:00
parent 4f6bc98979
commit 563cf6e820
30 changed files with 243 additions and 51 deletions

View File

@@ -21,6 +21,8 @@ import { ItemPageModule } from './item-page/item-page.module';
import { CollectionPageModule } from './collection-page/collection-page.module';
import { CommunityPageModule } from './community-page/community-page.module';
import { SearchPageModule } from './search-page/search-page.module';
import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
@@ -42,6 +44,7 @@ export function getConfig() {
ItemPageModule,
CollectionPageModule,
CommunityPageModule,
SearchPageModule,
AppRoutingModule,
StoreModule.provideStore(rootReducer),
RouterStoreModule.connectRouter(),

View File

@@ -3,11 +3,12 @@ import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { CacheableObject } from '../cache/object-cache.reducer';
import { RemoteData } from '../data/remote-data';
import { ResourceType } from './resource-type';
import { ListableObject } from '../../object-list/listable-object/listable-object.model';
/**
* An abstract model class for a DSpaceObject.
*/
export abstract class DSpaceObject implements CacheableObject {
export abstract class DSpaceObject implements CacheableObject, ListableObject {
self: string;

View File

@@ -1,6 +1,6 @@
<a [routerLink]="['/collections/' + collection.id]" class="lead">
{{collection.name}}
<a [routerLink]="['/collections/' + object.id]" class="lead">
{{object.name}}
</a>
<div *ngIf="collection.shortDescription" class="text-muted">
{{collection.shortDescription}}
<div *ngIf="object.shortDescription" class="text-muted">
{{object.shortDescription}}
</div>

View File

@@ -1,16 +1,14 @@
import { Component, Input } from '@angular/core';
import { Component } from '@angular/core';
import { Collection } from '../../core/shared/collection.model';
import { ObjectListElementComponent } from '../object-list-element/object-list-element.component';
import { listElementFor } from '../list-element-decorator';
@Component({
selector: 'ds-collection-list-element',
styleUrls: ['./collection-list-element.component.scss'],
templateUrl: './collection-list-element.component.html'
})
export class CollectionListElementComponent {
@Input() collection: Collection;
data: any = {};
}
@listElementFor(Collection)
export class CollectionListElementComponent extends ObjectListElementComponent {}

View File

@@ -1,6 +1,6 @@
<a [routerLink]="['/communities/' + community.id]" class="lead">
{{community.name}}
<a [routerLink]="['/communities/' + object.id]" class="lead">
{{object.name}}
</a>
<div *ngIf="community.shortDescription" class="text-muted">
{{community.shortDescription}}
<div *ngIf="object.shortDescription" class="text-muted">
{{object.shortDescription}}
</div>

View File

@@ -1,16 +1,14 @@
import { Component, Input } from '@angular/core';
import { Community } from '../../core/shared/community.model';
import { ObjectListElementComponent } from '../object-list-element/object-list-element.component';
import { listElementFor } from '../list-element-decorator';
@Component({
selector: 'ds-community-list-element',
styleUrls: ['./community-list-element.component.scss'],
templateUrl: './community-list-element.component.html'
})
export class CommunityListElementComponent {
@Input() community: Community;
data: any = {};
}
@listElementFor(Community)
export class CommunityListElementComponent extends ObjectListElementComponent {}

View File

@@ -1,14 +1,14 @@
<a [routerLink]="['/items/' + item.id]" class="lead">
{{item.findMetadata("dc.title")}}
<a [routerLink]="['/items/' + object.id]" class="lead">
{{object.findMetadata("dc.title")}}
</a>
<div>
<span class="text-muted">
<span *ngIf="item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);" class="item-list-authors">
<span *ngFor="let authorMd of item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);" class="item-list-authors">
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
<span *ngIf="!last">; </span>
</span>
</span>
(<span *ngIf="item.findMetadata('dc.publisher')" class="item-list-publisher">{{item.findMetadata("dc.publisher")}}, </span><span *ngIf="item.findMetadata('dc.date.issued')" class="item-list-date">{{item.findMetadata("dc.date.issued")}}</span>)
(<span *ngIf="object.findMetadata('dc.publisher')" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span *ngIf="object.findMetadata('dc.date.issued')" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
</span>
<div *ngIf="item.findMetadata('dc.description.abstract')" class="item-list-abstract">{{item.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</div>
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</div>
</div>

View File

@@ -1,15 +1,14 @@
import { Component, Input } from '@angular/core';
import { Item } from '../../core/shared/item.model';
import { ObjectListElementComponent } from '../object-list-element/object-list-element.component';
import { listElementFor } from '../list-element-decorator';
@Component({
selector: 'ds-item-list-element',
styleUrls: ['./item-list-element.component.scss'],
templateUrl: './item-list-element.component.html'
})
export class ItemListElementComponent {
@Input() item: Item;
data: any = {};
}
@listElementFor(Item)
export class ItemListElementComponent extends ObjectListElementComponent {}

View File

@@ -0,0 +1,12 @@
import { ListableObject } from './listable-object/listable-object.model';
import { GenericConstructor } from '../core/shared/generic-constructor';
const listElementForMetadataKey = Symbol('listElementFor');
export function listElementFor(value: GenericConstructor<ListableObject>) {
return Reflect.metadata(listElementForMetadataKey, value);
}
export function getListElementFor(target: any) {
return Reflect.getOwnMetadata(listElementForMetadataKey, target);
}

View File

@@ -0,0 +1,3 @@
export interface ListableObject {
}

View File

@@ -1,5 +0,0 @@
<div [ngSwitch]="object.type">
<ds-item-list-element *ngSwitchCase="type.Item" [item]="object"></ds-item-list-element>
<ds-collection-list-element *ngSwitchCase="type.Collection" [collection]="object"></ds-collection-list-element>
<ds-community-list-element *ngSwitchCase="type.Community" [community]="object"></ds-community-list-element>
</div>

View File

@@ -1,6 +1,5 @@
import { Component, Input, OnInit } from '@angular/core';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { ResourceType } from '../../core/shared/resource-type';
import { Component, Inject } from '@angular/core';
import { ListableObject } from '../listable-object/listable-object.model';
@Component({
selector: 'ds-object-list-element',
@@ -9,10 +8,6 @@ import { ResourceType } from '../../core/shared/resource-type';
})
export class ObjectListElementComponent {
public type = ResourceType;
@Input() object: DSpaceObject;
data: any = {};
// In the current version of Angular4, @Input is not supported by the NgComponentOutlet - instead we're using DI
constructor(@Inject('objectElementProvider') public object: ListableObject) { }
}

View File

@@ -10,7 +10,7 @@
(sortFieldChange)="onSortDirectionChange($event)">
<ul *ngIf="objects.hasSucceeded | async"> <!--class="list-unstyled"-->
<li *ngFor="let object of (objects.payload | async) | paginate: { itemsPerPage: (pageInfo | async)?.elementsPerPage, currentPage: (pageInfo | async)?.currentPage, totalItems: (pageInfo | async)?.totalElements }">
<ds-object-list-element [object]="object"></ds-object-list-element>
<ds-wrapper-list-element [object]="object"></ds-wrapper-list-element>
</li>
</ul>

View File

@@ -0,0 +1 @@
<ng-container *ngComponentOutlet="getListElement(); injector: objectInjector;"></ng-container>

View File

@@ -0,0 +1,3 @@
@import '../../../styles/variables.scss';
@import '../../../../node_modules/bootstrap/scss/variables';

View File

@@ -0,0 +1,25 @@
import { Component, Input, Injector, ReflectiveInjector, OnInit } from '@angular/core';
import { ListableObject } from '../listable-object/listable-object.model';
import { getListElementFor } from '../list-element-decorator'
@Component({
selector: 'ds-wrapper-list-element',
styleUrls: ['./wrapper-list-element.component.scss'],
templateUrl: './wrapper-list-element.component.html'
})
export class WrapperListElementComponent implements OnInit {
@Input() object: ListableObject;
objectInjector: Injector;
constructor(private injector: Injector) {}
ngOnInit(): void {
this.objectInjector = ReflectiveInjector.resolveAndCreate(
[{provide: 'objectElementProvider', useFactory: () => ({ providedObject: this.object }) }], this.injector);
}
private getListElement(): string {
return getListElementFor(this.object.constructor).constructor.name;
}
}

View File

@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { SearchPageComponent } from './search-page.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: 'search', component: SearchPageComponent }
])
]
})
export class SearchPageRoutingModule { }

View File

@@ -0,0 +1,4 @@
<div class="search-page">
<ds-search-form></ds-search-form>
<ds-search-results [searchResults]="results"></ds-search-results>
</div>

View File

@@ -0,0 +1 @@
@import '../../styles/variables.scss';

View File

@@ -0,0 +1,42 @@
import { Component, OnInit } from '@angular/core';
import { SearchService } from '../search/search.service';
import { ActivatedRoute } from '@angular/router';
import { SortOptions } from '../core/cache/models/sort-options.model';
import { RemoteData } from '../core/data/remote-data';
import { SearchResult } from '../search/search-result.model';
import { DSpaceObject } from '../core/shared/dspace-object.model';
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template.
*/
@Component({
selector: 'ds-search-page',
styleUrls: ['./search-page.component.scss'],
templateUrl: './search-page.component.html',
})
export class SearchPageComponent implements OnInit {
private sub;
private results: RemoteData<Array<SearchResult<DSpaceObject>>>;
constructor(
private service: SearchService,
private route: ActivatedRoute,
) { }
ngOnInit(): void {
this.sub = this.route
.queryParams
.subscribe((params) => {
const query: string = params.query || '';
const scope: string = params.scope;
const page: number = +params.page || 0;
this.results = this.service.search(query, scope, {elementsPerPage: 10, currentPage: page, sort: new SortOptions()});
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}

View File

@@ -0,0 +1,27 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { SharedModule } from '../shared/shared.module';
import { SearchPageRoutingModule } from './search-page-routing.module';
import { SearchPageComponent } from './search-page.component';
import { SearchFormComponent } from '../shared/search-form/search-form.component';
import { SearchResultsComponent } from './search-results/search-results.compontent';
@NgModule({
imports: [
SearchPageRoutingModule,
CommonModule,
TranslateModule,
RouterModule,
SharedModule,
],
declarations: [
SearchPageComponent,
SearchFormComponent,
SearchResultsComponent
]
})
export class SearchPageModule { }

View File

@@ -0,0 +1 @@
<ds-object-list [objects]="searchResults"></ds-object-list>

View File

@@ -0,0 +1,23 @@
import { Component, OnInit, Input } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template.
*/
@Component({
selector: 'ds-search-results',
templateUrl: './search-results.component.html',
})
export class SearchResultsComponent implements OnInit {
@Input() searchResults: RemoteData<DSpaceObject[]>;
ngOnInit(): void {
// onInit
}
}

View File

@@ -1,7 +1,8 @@
import { DSpaceObject } from '../core/shared/dspace-object.model';
import { Metadatum } from '../core/shared/metadatum.model';
import { ListableObject } from '../object-list/listable-object/listable-object.model';
export class SearchResult<T extends DSpaceObject>{
export class SearchResult<T extends DSpaceObject> implements ListableObject {
dspaceObject: T;
hitHighlights: Metadatum[];

View File

@@ -11,12 +11,12 @@ import {
import { Observable } from 'rxjs/Observable';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { PageInfo } from '../../core/shared/page-info.model';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
import { SortOptions, SortDirection } from '../../core/cache/models/sort-options.model';
import { ListableObject } from '../../object-list/listable-object/listable-object.model';
@Component({
changeDetection: ChangeDetectionStrategy.Default,
@@ -27,7 +27,7 @@ import { SortOptions, SortDirection } from '../../core/cache/models/sort-options
})
export class ObjectListComponent implements OnChanges, OnInit {
@Input() objects: RemoteData<DSpaceObject[]>;
@Input() objects: RemoteData<ListableObject[]>;
@Input() config: PaginationComponentOptions;
@Input() sortConfig: SortOptions;
@Input() hideGear = false;

View File

@@ -0,0 +1,15 @@
<form [formGroup]="searchFormGroup">
<div class="input-group">
<input type="text" class="form-control" aria-label="Search input">
<div ngbDropdown class="input-group-btn">
<button type="submit" class="btn btn-secondary" id="searchDropdown" ngbDropdownToggle>
Go
</button>
<div ngbDropdownMenu aria-labelledby="searchDropdown">
<a class="dropdown-item" href="#">Search DSpace</a>
<a class="dropdown-item" href="#">Search this collection</a>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1 @@
@import '../../../styles/variables.scss';

View File

@@ -0,0 +1,28 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template.
*/
@Component({
selector: 'ds-search-form',
styleUrls: ['./search-form.component.scss'],
templateUrl: './search-form.component.html',
})
export class SearchFormComponent implements OnInit {
searchFormGroup: FormGroup;
//
// constructor() {
//
// }
//
ngOnInit(): void {
this.searchFormGroup = new FormGroup({
firstName: new FormControl()
});
}
}

View File

@@ -24,6 +24,7 @@ import { ItemListElementComponent } from '../object-list/item-list-element/item-
import { CommunityListElementComponent } from '../object-list/community-list-element/community-list-element.component';
import { CollectionListElementComponent } from '../object-list/collection-list-element/collection-list-element.component';
import { TruncatePipe } from './utils/truncate.pipe';
import { WrapperListElementComponent } from '../object-list/wrapper-list-element/wrapper-list-element.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -53,6 +54,7 @@ const COMPONENTS = [
ComcolPageLogoComponent,
ObjectListComponent,
ObjectListElementComponent,
WrapperListElementComponent,
ItemListElementComponent,
CollectionListElementComponent,
CommunityListElementComponent

View File

@@ -3,5 +3,6 @@ export const ROUTES: string[] = [
'items/:id',
'collections/:id',
'communities/:id',
'search',
'**'
];