mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-13 04:53:06 +00:00
44024: simple search UI with working search results
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, Inject } from '@angular/core';
|
||||||
|
|
||||||
import { Collection } from '../../core/shared/collection.model';
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
import { ObjectListElementComponent } from '../object-list-element/object-list-element.component';
|
import { ObjectListElementComponent } from '../object-list-element/object-list-element.component';
|
||||||
@@ -11,4 +11,4 @@ import { listElementFor } from '../list-element-decorator';
|
|||||||
})
|
})
|
||||||
|
|
||||||
@listElementFor(Collection)
|
@listElementFor(Collection)
|
||||||
export class CollectionListElementComponent extends ObjectListElementComponent {}
|
export class CollectionListElementComponent extends ObjectListElementComponent<Collection> {}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input, Inject } from '@angular/core';
|
||||||
|
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
import { ObjectListElementComponent } from '../object-list-element/object-list-element.component';
|
import { ObjectListElementComponent } from '../object-list-element/object-list-element.component';
|
||||||
@@ -11,4 +11,4 @@ import { listElementFor } from '../list-element-decorator';
|
|||||||
})
|
})
|
||||||
|
|
||||||
@listElementFor(Community)
|
@listElementFor(Community)
|
||||||
export class CommunityListElementComponent extends ObjectListElementComponent {}
|
export class CommunityListElementComponent extends ObjectListElementComponent<Community> {}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input, Inject } from '@angular/core';
|
||||||
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { ObjectListElementComponent } from '../object-list-element/object-list-element.component';
|
import { ObjectListElementComponent } from '../object-list-element/object-list-element.component';
|
||||||
@@ -11,4 +11,4 @@ import { listElementFor } from '../list-element-decorator';
|
|||||||
})
|
})
|
||||||
|
|
||||||
@listElementFor(Item)
|
@listElementFor(Item)
|
||||||
export class ItemListElementComponent extends ObjectListElementComponent {}
|
export class ItemListElementComponent extends ObjectListElementComponent<Item> {}
|
||||||
|
@@ -1,12 +1,16 @@
|
|||||||
import { ListableObject } from './listable-object/listable-object.model';
|
import { ListableObject } from './listable-object/listable-object.model';
|
||||||
import { GenericConstructor } from '../core/shared/generic-constructor';
|
import { GenericConstructor } from '../core/shared/generic-constructor';
|
||||||
|
|
||||||
const listElementForMetadataKey = Symbol('listElementFor');
|
const listElementMap = new Map();
|
||||||
|
export function listElementFor(listable: GenericConstructor<ListableObject>) {
|
||||||
export function listElementFor(value: GenericConstructor<ListableObject>) {
|
return function decorator(objectElement: any) {
|
||||||
return Reflect.metadata(listElementForMetadataKey, value);
|
if (!objectElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
listElementMap.set(listable, objectElement);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getListElementFor(target: any) {
|
export function getListElementFor(listable: GenericConstructor<ListableObject>) {
|
||||||
return Reflect.getOwnMetadata(listElementForMetadataKey, target);
|
return listElementMap.get(listable);
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1 @@
|
|||||||
export interface ListableObject {
|
export interface ListableObject {}
|
||||||
|
|
||||||
}
|
|
||||||
|
@@ -6,8 +6,9 @@ import { ListableObject } from '../listable-object/listable-object.model';
|
|||||||
styleUrls: ['./object-list-element.component.scss'],
|
styleUrls: ['./object-list-element.component.scss'],
|
||||||
templateUrl: './object-list-element.component.html'
|
templateUrl: './object-list-element.component.html'
|
||||||
})
|
})
|
||||||
export class ObjectListElementComponent {
|
export class ObjectListElementComponent <T extends ListableObject> {
|
||||||
|
object: T;
|
||||||
// In the current version of Angular4, @Input is not supported by the NgComponentOutlet - instead we're using DI
|
public constructor(@Inject('objectElementProvider') public listable: ListableObject) {
|
||||||
constructor(@Inject('objectElementProvider') public object: ListableObject) { }
|
this.object = listable as T;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
<a [routerLink]="['/collections/' + dso.id]" class="lead">
|
||||||
|
{{dso.name}}
|
||||||
|
</a>
|
||||||
|
<div *ngIf="dso.shortDescription" class="text-muted">
|
||||||
|
{{dso.shortDescription}}
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../../styles/variables.scss';
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
import { listElementFor } from '../../list-element-decorator';
|
||||||
|
import { CollectionSearchResult } from './collection-search-result.model';
|
||||||
|
import { SearchResultListElementComponent } from '../search-result-list-element.component';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-collection-search-result-list-element',
|
||||||
|
styleUrls: ['collection-search-result-list-element.component.scss'],
|
||||||
|
templateUrl: 'collection-search-result-list-element.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
@listElementFor(CollectionSearchResult)
|
||||||
|
export class CollectionSearchResultListElementComponent extends SearchResultListElementComponent<CollectionSearchResult, Collection> {}
|
@@ -0,0 +1,5 @@
|
|||||||
|
import { SearchResult } from '../../../search/search-result.model';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
|
||||||
|
export class CollectionSearchResult extends SearchResult<Collection> {
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
<a [routerLink]="['/communities/' + dso.id]" class="lead">
|
||||||
|
{{dso.name}}
|
||||||
|
</a>
|
||||||
|
<div *ngIf="dso.shortDescription" class="text-muted">
|
||||||
|
{{dso.shortDescription}}
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../../styles/variables.scss';
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
import { listElementFor } from '../../list-element-decorator';
|
||||||
|
import { CommunitySearchResult } from './community-search-result.model';
|
||||||
|
import { SearchResultListElementComponent } from '../search-result-list-element.component';
|
||||||
|
import { Community } from '../../../core/shared/community.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-community-search-result-list-element',
|
||||||
|
styleUrls: ['community-search-result-list-element.component.scss'],
|
||||||
|
templateUrl: 'community-search-result-list-element.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
@listElementFor(CommunitySearchResult)
|
||||||
|
export class CommunitySearchResultListElementComponent extends SearchResultListElementComponent<CommunitySearchResult, Community> {
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
import { SearchResult } from '../../../search/search-result.model';
|
||||||
|
import { Community } from '../../../core/shared/community.model';
|
||||||
|
|
||||||
|
export class CommunitySearchResult extends SearchResult<Community> {
|
||||||
|
}
|
@@ -0,0 +1,14 @@
|
|||||||
|
<a [routerLink]="['/items/' + dso.id]" class="lead">
|
||||||
|
{{dso.findMetadata("dc.title")}}
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<span class="text-muted">
|
||||||
|
<span *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);" class="item-list-authors">
|
||||||
|
<span *ngFor="let authorMd of dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
|
||||||
|
<span *ngIf="!last">; </span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
(<span *ngIf="dso.findMetadata('dc.publisher')" class="item-list-publisher">{{dso.findMetadata("dc.publisher")}}, </span><span *ngIf="dso.findMetadata('dc.date.issued')" class="item-list-date">{{dso.findMetadata("dc.date.issued")}}</span>)
|
||||||
|
</span>
|
||||||
|
<div *ngIf="dso.findMetadata('dc.description.abstract')" class="item-list-abstract">{{dso.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../../styles/variables.scss';
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
import { listElementFor } from '../../list-element-decorator';
|
||||||
|
import { ItemSearchResult } from './item-search-result.model';
|
||||||
|
import { SearchResultListElementComponent } from '../search-result-list-element.component';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-search-result-list-element',
|
||||||
|
styleUrls: ['item-search-result-list-element.component.scss'],
|
||||||
|
templateUrl: 'item-search-result-list-element.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
@listElementFor(ItemSearchResult)
|
||||||
|
export class ItemSearchResultListElementComponent extends SearchResultListElementComponent<ItemSearchResult, Item> {}
|
@@ -0,0 +1,5 @@
|
|||||||
|
import { SearchResult } from '../../../search/search-result.model';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
|
||||||
|
export class ItemSearchResult extends SearchResult<Item> {
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
|
||||||
|
import { ObjectListElementComponent } from '../object-list-element/object-list-element.component';
|
||||||
|
import { ListableObject } from '../listable-object/listable-object.model';
|
||||||
|
import { SearchResult } from '../../search/search-result.model';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-search-result-list-element',
|
||||||
|
template: ``
|
||||||
|
})
|
||||||
|
|
||||||
|
export class SearchResultListElementComponent<T extends SearchResult<K>, K extends DSpaceObject> extends ObjectListElementComponent<T> {
|
||||||
|
dso: K;
|
||||||
|
public constructor(@Inject('objectElementProvider') public listable: ListableObject) {
|
||||||
|
super(listable);
|
||||||
|
this.dso = this.object.dspaceObject;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
import { Component, Input, Injector, ReflectiveInjector, OnInit } from '@angular/core';
|
import { Component, Input, Injector, ReflectiveInjector, OnInit } from '@angular/core';
|
||||||
import { ListableObject } from '../listable-object/listable-object.model';
|
import { ListableObject } from '../listable-object/listable-object.model';
|
||||||
import { getListElementFor } from '../list-element-decorator'
|
import { getListElementFor } from '../list-element-decorator'
|
||||||
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-wrapper-list-element',
|
selector: 'ds-wrapper-list-element',
|
||||||
@@ -15,11 +16,12 @@ export class WrapperListElementComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.objectInjector = ReflectiveInjector.resolveAndCreate(
|
this.objectInjector = ReflectiveInjector.resolveAndCreate(
|
||||||
[{provide: 'objectElementProvider', useFactory: () => ({ providedObject: this.object }) }], this.injector);
|
[{provide: 'objectElementProvider', useFactory: () => (this.object) }], this.injector);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getListElement(): string {
|
getListElement(): string {
|
||||||
return getListElementFor(this.object.constructor).constructor.name;
|
const f: GenericConstructor<ListableObject> = this.object.constructor as GenericConstructor<ListableObject>;
|
||||||
|
return getListElementFor(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,7 @@ import { DSpaceObject } from '../core/shared/dspace-object.model';
|
|||||||
})
|
})
|
||||||
export class SearchPageComponent implements OnInit {
|
export class SearchPageComponent implements OnInit {
|
||||||
private sub;
|
private sub;
|
||||||
private results: RemoteData<Array<SearchResult<DSpaceObject>>>;
|
results: RemoteData<Array<SearchResult<DSpaceObject>>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private service: SearchService,
|
private service: SearchService,
|
||||||
|
@@ -9,6 +9,10 @@ import { SearchPageRoutingModule } from './search-page-routing.module';
|
|||||||
import { SearchPageComponent } from './search-page.component';
|
import { SearchPageComponent } from './search-page.component';
|
||||||
import { SearchFormComponent } from '../shared/search-form/search-form.component';
|
import { SearchFormComponent } from '../shared/search-form/search-form.component';
|
||||||
import { SearchResultsComponent } from './search-results/search-results.compontent';
|
import { SearchResultsComponent } from './search-results/search-results.compontent';
|
||||||
|
import { SearchModule } from '../search/search.module';
|
||||||
|
import { ItemSearchResultListElementComponent } from '../object-list/search-result-list-element/item-search-result/item-search-result-list-element.component';
|
||||||
|
import { CollectionSearchResultListElementComponent } from '../object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component';
|
||||||
|
import { CommunitySearchResultListElementComponent } from '../object-list/search-result-list-element/community-search-result/community-search-result-list-element.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -17,11 +21,20 @@ import { SearchResultsComponent } from './search-results/search-results.componte
|
|||||||
TranslateModule,
|
TranslateModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
SearchModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
SearchPageComponent,
|
SearchPageComponent,
|
||||||
SearchFormComponent,
|
SearchFormComponent,
|
||||||
SearchResultsComponent
|
SearchResultsComponent,
|
||||||
|
ItemSearchResultListElementComponent,
|
||||||
|
CollectionSearchResultListElementComponent,
|
||||||
|
CommunitySearchResultListElementComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
ItemSearchResultListElementComponent,
|
||||||
|
CollectionSearchResultListElementComponent,
|
||||||
|
CommunitySearchResultListElementComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SearchPageModule { }
|
export class SearchPageModule { }
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { Component, OnInit, Input } from '@angular/core';
|
import { Component, OnInit, Input } from '@angular/core';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { SearchResult } from '../../search/search-result.model';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,7 +15,7 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
|||||||
})
|
})
|
||||||
|
|
||||||
export class SearchResultsComponent implements OnInit {
|
export class SearchResultsComponent implements OnInit {
|
||||||
@Input() searchResults: RemoteData<DSpaceObject[]>;
|
@Input() searchResults: RemoteData<Array<SearchResult<DSpaceObject>>>;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// onInit
|
// onInit
|
||||||
|
@@ -9,6 +9,7 @@ import { SearchOptions } from './search.models';
|
|||||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||||
import { Metadatum } from '../core/shared/metadatum.model';
|
import { Metadatum } from '../core/shared/metadatum.model';
|
||||||
import { Item } from '../core/shared/item.model';
|
import { Item } from '../core/shared/item.model';
|
||||||
|
import { ItemSearchResult } from '../object-list/search-result-list-element/item-search-result/item-search-result.model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchService {
|
export class SearchService {
|
||||||
@@ -62,12 +63,12 @@ export class SearchService {
|
|||||||
elementsPerPage: returningPageInfo.elementsPerPage
|
elementsPerPage: returningPageInfo.elementsPerPage
|
||||||
});
|
});
|
||||||
const payload = itemsRD.payload.map((items: Item[]) => {
|
const payload = itemsRD.payload.map((items: Item[]) => {
|
||||||
return items.sort(()=>{
|
return items.sort(() => {
|
||||||
const values = [-1, 0, 1];
|
const values = [-1, 0, 1];
|
||||||
return values[Math.floor(Math.random() * values.length)];
|
return values[Math.floor(Math.random() * values.length)];
|
||||||
})
|
})
|
||||||
.map((item: Item, index: number) => {
|
.map((item: Item, index: number) => {
|
||||||
const mockResult: SearchResult<DSpaceObject> = new SearchResult();
|
const mockResult: SearchResult<DSpaceObject> = new ItemSearchResult();
|
||||||
mockResult.dspaceObject = item;
|
mockResult.dspaceObject = item;
|
||||||
const highlight = new Metadatum();
|
const highlight = new Metadatum();
|
||||||
highlight.key = 'dc.description.abstract';
|
highlight.key = 'dc.description.abstract';
|
||||||
|
@@ -1,15 +1,19 @@
|
|||||||
<form [formGroup]="searchFormGroup">
|
<form [formGroup]="searchFormGroup" action="/search">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" aria-label="Search input">
|
<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 class="input-group-btn" ngbDropdown>
|
||||||
|
<button type="submit" class="btn btn-secondary">Search DSpace</button>
|
||||||
|
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown"
|
||||||
|
aria-haspopup="true" aria-expanded="false" id="searchDropdown" ngbDropdownToggle>
|
||||||
|
<span class="sr-only">Toggle Dropdown</span>
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu class="dropdown-menu dropdown-menu-right" aria-labelledby="searchDropdown">
|
||||||
|
<a class="dropdown-item" href="#">Search DSpace</a>
|
||||||
|
<a class="dropdown-item" href="#">Search this Collection</a>
|
||||||
|
<a class="dropdown-item" href="#">Search this Community</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -25,6 +25,7 @@ import { CommunityListElementComponent } from '../object-list/community-list-ele
|
|||||||
import { CollectionListElementComponent } from '../object-list/collection-list-element/collection-list-element.component';
|
import { CollectionListElementComponent } from '../object-list/collection-list-element/collection-list-element.component';
|
||||||
import { TruncatePipe } from './utils/truncate.pipe';
|
import { TruncatePipe } from './utils/truncate.pipe';
|
||||||
import { WrapperListElementComponent } from '../object-list/wrapper-list-element/wrapper-list-element.component';
|
import { WrapperListElementComponent } from '../object-list/wrapper-list-element/wrapper-list-element.component';
|
||||||
|
import { SearchResultListElementComponent } from '../object-list/search-result-list-element/search-result-list-element.component';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -54,10 +55,15 @@ const COMPONENTS = [
|
|||||||
ComcolPageLogoComponent,
|
ComcolPageLogoComponent,
|
||||||
ObjectListComponent,
|
ObjectListComponent,
|
||||||
ObjectListElementComponent,
|
ObjectListElementComponent,
|
||||||
WrapperListElementComponent,
|
WrapperListElementComponent
|
||||||
|
];
|
||||||
|
|
||||||
|
const ENTRY_COMPONENTS = [
|
||||||
|
// put shared entry components (components that are created dynamically) here
|
||||||
ItemListElementComponent,
|
ItemListElementComponent,
|
||||||
CollectionListElementComponent,
|
CollectionListElementComponent,
|
||||||
CommunityListElementComponent
|
CommunityListElementComponent,
|
||||||
|
SearchResultListElementComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
@@ -72,7 +78,8 @@ const PROVIDERS = [
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
...PIPES,
|
...PIPES,
|
||||||
...COMPONENTS
|
...COMPONENTS,
|
||||||
|
...ENTRY_COMPONENTS
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
...MODULES,
|
...MODULES,
|
||||||
@@ -81,6 +88,9 @@ const PROVIDERS = [
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
...PROVIDERS
|
...PROVIDERS
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
...ENTRY_COMPONENTS
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SharedModule {
|
export class SharedModule {
|
||||||
|
Reference in New Issue
Block a user