forked from geolba/tethys.frontend
Progress OpenSearch. Pending solving onFilter issue with active filters
This commit is contained in:
parent
da430d6142
commit
d135ab2d50
|
@ -20,6 +20,9 @@ const get = <T>(url: string, queryParams?: any): Observable<T> => {
|
||||||
// Function to make a POST request using Axios wrapped in an Observable
|
// Function to make a POST request using Axios wrapped in an Observable
|
||||||
const post = <T>(url: string, body: any, queryParams?: any): Observable<T> => {
|
const post = <T>(url: string, body: any, queryParams?: any): Observable<T> => {
|
||||||
// Use defer to create an Observable that makes the Axios POST request when subscribed to
|
// Use defer to create an Observable that makes the Axios POST request when subscribed to
|
||||||
|
// console.log(body);
|
||||||
|
// console.log(queryParams);
|
||||||
|
|
||||||
return defer(() => axiosInstance.post<T>(url, body, {
|
return defer(() => axiosInstance.post<T>(url, body, {
|
||||||
params: queryParams,
|
params: queryParams,
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
@ -20,6 +20,7 @@ export default class FacetCategory extends Vue {
|
||||||
filterName!: string;
|
filterName!: string;
|
||||||
|
|
||||||
get alias(): string {
|
get alias(): string {
|
||||||
|
console.log("filterName:", this.filterName);
|
||||||
return this.filterName == "datatype" ? "doctype" : this.filterName;
|
return this.filterName == "datatype" ? "doctype" : this.filterName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +54,7 @@ export default class FacetCategory extends Vue {
|
||||||
|
|
||||||
@Emit("filter")
|
@Emit("filter")
|
||||||
activateItem(filterItem: FacetItem): FacetItem {
|
activateItem(filterItem: FacetItem): FacetItem {
|
||||||
|
// console.log(filterItem);
|
||||||
filterItem.category = this.alias;
|
filterItem.category = this.alias;
|
||||||
filterItem.active = true;
|
filterItem.active = true;
|
||||||
// this.$emit("filter", filterItem);
|
// this.$emit("filter", filterItem);
|
||||||
|
|
|
@ -8,6 +8,9 @@
|
||||||
<!-- e.g.language -->
|
<!-- e.g.language -->
|
||||||
<ul class="filter-items list-unstyled" v-bind:class="{ limited: facetItems.length > 1 && collapsed }">
|
<ul class="filter-items list-unstyled" v-bind:class="{ limited: facetItems.length > 1 && collapsed }">
|
||||||
<li v-for="(item, index) in facetItems" v-bind:key="index" class="list-group-item titlecase">
|
<li v-for="(item, index) in facetItems" v-bind:key="index" class="list-group-item titlecase">
|
||||||
|
{{ item.active }}
|
||||||
|
{{ item.val }}
|
||||||
|
{{ item.count }} //
|
||||||
<!-- <span :class="item.Active ? 'disabled' : ''" @click.prevent="activateItem(item)">{{ item.val }} ({{ item.count }}) </span> -->
|
<!-- <span :class="item.Active ? 'disabled' : ''" @click.prevent="activateItem(item)">{{ item.val }} ({{ item.count }}) </span> -->
|
||||||
<span v-bind:class="item.active ? 'disabled' : ''" @click.prevent="activateItem(item)">{{ item.val }} ({{ item.count }}) </span>
|
<span v-bind:class="item.active ? 'disabled' : ''" @click.prevent="activateItem(item)">{{ item.val }} ({{ item.count }}) </span>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -210,7 +210,7 @@ export default class VsInput extends Vue {
|
||||||
|
|
||||||
// Handler for search input change
|
// Handler for search input change
|
||||||
searchChanged(): void {
|
searchChanged(): void {
|
||||||
console.log("Search changed!");
|
// console.log("Search changed!");
|
||||||
this.selectedIndex = -1;
|
this.selectedIndex = -1;
|
||||||
// Let's warn the parent that a change was made
|
// Let's warn the parent that a change was made
|
||||||
// this.$emit("input", this.display);
|
// this.$emit("input", this.display);
|
||||||
|
@ -224,7 +224,7 @@ export default class VsInput extends Vue {
|
||||||
|
|
||||||
// Perform the search request
|
// Perform the search request
|
||||||
private resourceSearch() {
|
private resourceSearch() {
|
||||||
console.log("resourceSearch");
|
// console.log("resourceSearch");
|
||||||
if (!this.display) {
|
if (!this.display) {
|
||||||
this.results = [];
|
this.results = [];
|
||||||
return;
|
return;
|
||||||
|
@ -252,14 +252,11 @@ export default class VsInput extends Vue {
|
||||||
this.highlights = highlights; // Store highlights
|
this.highlights = highlights; // Store highlights
|
||||||
// console.log(datasets);
|
// console.log(datasets);
|
||||||
|
|
||||||
// this.$emit("search", this.display);
|
|
||||||
// this.loading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle errors from the search request
|
// Handle errors from the search request
|
||||||
private errorHandler(err: string): void {
|
private errorHandler(err: string): void {
|
||||||
this.error = err;
|
this.error = err;
|
||||||
// this.loading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -321,18 +318,14 @@ export default class VsInput extends Vue {
|
||||||
|
|
||||||
@Emit("search-change")
|
@Emit("search-change")
|
||||||
private select(obj: Suggestion): Suggestion {
|
private select(obj: Suggestion): Suggestion {
|
||||||
// if (!obj) {
|
console.log("select:");
|
||||||
// return;
|
this.value = obj;
|
||||||
// }
|
|
||||||
console.log("select");
|
|
||||||
this.value = obj; //(obj["title_output"]) ? obj["title_output"] : obj.id
|
|
||||||
console.log(obj);
|
console.log(obj);
|
||||||
|
|
||||||
this.display = obj.value; // this.formatDisplay(obj)
|
this.display = obj.value;
|
||||||
// this.selectedDisplay = this.display;
|
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
// this.$emit("update", this.value);
|
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -355,12 +348,7 @@ export default class VsInput extends Vue {
|
||||||
if (!this.value) {
|
if (!this.value) {
|
||||||
this.clear();
|
this.clear();
|
||||||
}
|
}
|
||||||
// if (this.selectedDisplay !== this.display && this.value) {
|
|
||||||
// this.display = this.selectedDisplay;
|
|
||||||
// }
|
|
||||||
this.results = [];
|
this.results = [];
|
||||||
this.error = "";
|
this.error = "";
|
||||||
//this.removeEventListener()
|
|
||||||
// this.$emit("close");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,13 +23,15 @@ export default class VsResult extends Vue {
|
||||||
.join(".");
|
.join(".");
|
||||||
}
|
}
|
||||||
|
|
||||||
private convert(unixtimestamp: number): string {
|
// private convert(unixtimestamp: number): string { // SOLR
|
||||||
|
private convert(unixtimestamp: string): string { // OpenSearch
|
||||||
// Unixtimestamp
|
// Unixtimestamp
|
||||||
// var unixtimestamp = document.getElementById('timestamp').value;
|
// var unixtimestamp = document.getElementById('timestamp').value;
|
||||||
// Months array
|
// Months array
|
||||||
const months_arr = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
const months_arr = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||||
// Convert timestamp to milliseconds
|
// Convert timestamp to milliseconds
|
||||||
const date = new Date(unixtimestamp * 1000);
|
// const date = new Date(unixtimestamp * 1000); // SOLR
|
||||||
|
const date = new Date(Number(unixtimestamp) * 1000); // OpenSearch
|
||||||
// Year
|
// Year
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
// Month
|
// Month
|
||||||
|
|
|
@ -20,7 +20,8 @@
|
||||||
{{ convert(document.server_date_published) + ": " }}
|
{{ convert(document.server_date_published) + ": " }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text">
|
<span class="text">
|
||||||
{{ document.abstract_output }}
|
<!-- {{ document.abstract_output }} -->
|
||||||
|
{{ document.abstract[0] }}
|
||||||
<span class="ellipsis">...</span>
|
<span class="ellipsis">...</span>
|
||||||
<span class="fill"></span>
|
<span class="fill"></span>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -82,7 +82,7 @@ export class Suggestion {
|
||||||
export enum SearchType {
|
export enum SearchType {
|
||||||
Title = "title",
|
Title = "title",
|
||||||
Author = "author",
|
Author = "author",
|
||||||
Subject = "subject"
|
Subject = "subjects" // ** !! The field has this name in OpenSearch!!
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DbDataset {
|
export class DbDataset {
|
||||||
|
|
|
@ -26,6 +26,7 @@ export interface ResponseContent {
|
||||||
docs: Array<Dataset>;
|
docs: Array<Dataset>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Used
|
||||||
export class FacetResults {
|
export class FacetResults {
|
||||||
[key: string]: Array<FacetItem>;
|
[key: string]: Array<FacetItem>;
|
||||||
}
|
}
|
||||||
|
@ -39,6 +40,7 @@ export interface FacetInstance {
|
||||||
[key: string]: Array<FacetItem>;
|
[key: string]: Array<FacetItem>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Used
|
||||||
export class FacetItem {
|
export class FacetItem {
|
||||||
val: string;
|
val: string;
|
||||||
count: number;
|
count: number;
|
||||||
|
|
|
@ -146,8 +146,10 @@ class DatasetService {
|
||||||
|
|
||||||
const lowercaseTerm = typeof suggestion === 'string' ? suggestion.toLowerCase() : suggestion.value.toLowerCase();
|
const lowercaseTerm = typeof suggestion === 'string' ? suggestion.toLowerCase() : suggestion.value.toLowerCase();
|
||||||
|
|
||||||
console.log("facetedsearchOPEN > suggestion entered:");
|
// console.log("facetedsearchOPEN > suggestion entered:");
|
||||||
console.log(suggestion);
|
// console.log(suggestion);
|
||||||
|
// console.log("typeof:", typeof suggestion);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The query construction depends on whether the suggestion is a string or a Suggestion object. */
|
* The query construction depends on whether the suggestion is a string or a Suggestion object. */
|
||||||
|
@ -169,7 +171,7 @@ class DatasetService {
|
||||||
// When suggestion is a suggestion object
|
// When suggestion is a suggestion object
|
||||||
: {
|
: {
|
||||||
match: {
|
match: {
|
||||||
[suggestion.type.toLowerCase()]: {
|
[suggestion.type]: {
|
||||||
query: suggestion.value,
|
query: suggestion.value,
|
||||||
operator: 'and' // all the terms in the query must be present in the field
|
operator: 'and' // all the terms in the query must be present in the field
|
||||||
}
|
}
|
||||||
|
@ -219,7 +221,7 @@ class DatasetService {
|
||||||
// // }))
|
// // }))
|
||||||
// );
|
// );
|
||||||
const stations = api.post<OpenSearchResponse>(base, body);
|
const stations = api.post<OpenSearchResponse>(base, body);
|
||||||
|
|
||||||
return stations;
|
return stations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ export default class DatasetDetailComponent extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearch(suggestion: Suggestion | string): void {
|
onSearch(suggestion: Suggestion | string): void {
|
||||||
console.log("onSearch");
|
console.log("onSearch (dataset-detail.component)");
|
||||||
|
|
||||||
const host = window.location.host;
|
const host = window.location.host;
|
||||||
const parts = host.split(".");
|
const parts = host.split(".");
|
||||||
|
|
|
@ -68,6 +68,8 @@ export default class SearchViewComponent extends Vue {
|
||||||
|
|
||||||
// Computed property to get search term as string
|
// Computed property to get search term as string
|
||||||
get stringSearchTerm(): string {
|
get stringSearchTerm(): string {
|
||||||
|
// console.log("stringSearchTerm:", this.searchTerm);
|
||||||
|
|
||||||
if (typeof this.searchTerm === "string") {
|
if (typeof this.searchTerm === "string") {
|
||||||
return this.searchTerm;
|
return this.searchTerm;
|
||||||
} else if (this.searchTerm instanceof Suggestion) {
|
} else if (this.searchTerm instanceof Suggestion) {
|
||||||
|
@ -147,13 +149,24 @@ export default class SearchViewComponent extends Vue {
|
||||||
|
|
||||||
// Handle the search results
|
// Handle the search results
|
||||||
private dataHandlerOPEN(res: OpenSearchResponse, filterItem?: FacetItem): void {
|
private dataHandlerOPEN(res: OpenSearchResponse, filterItem?: FacetItem): void {
|
||||||
// console.log("dataHandlerOPEN (datasets, highlights):");
|
|
||||||
// console.log(datasets);
|
|
||||||
// console.log(highlights);
|
|
||||||
|
|
||||||
this.results = res.hits.hits.map(hit => hit._source);
|
this.results = res.hits.hits.map(hit => hit._source);
|
||||||
this.numFound = res.hits.total.value;
|
this.numFound = res.hits.total.value;
|
||||||
|
|
||||||
|
// console.log("dataHandlerOPEN (results, numFound):");
|
||||||
|
// console.log(this.results);
|
||||||
|
// console.log(this.numFound);
|
||||||
|
// console.log(res.hits.hits);
|
||||||
|
// console.log(res.hits.total.value);
|
||||||
|
|
||||||
|
// for (const key in this.results) {
|
||||||
|
// if (Object.prototype.hasOwnProperty.call(this.results, key)) {
|
||||||
|
// const element = this.results[key];
|
||||||
|
// // console.log(element.abstract[0]);
|
||||||
|
// // console.log(element.language);
|
||||||
|
// console.log(element.server_date_published);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
this.pagination.total = res.hits.total.value;
|
this.pagination.total = res.hits.total.value;
|
||||||
this.pagination.perPage = 10;
|
this.pagination.perPage = 10;
|
||||||
this.pagination.data = this.results;
|
this.pagination.data = this.results;
|
||||||
|
@ -178,9 +191,9 @@ export default class SearchViewComponent extends Vue {
|
||||||
|
|
||||||
// Method to handle search response
|
// Method to handle search response
|
||||||
private dataHandler(res: SolrResponse, filterItem?: FacetItem): void {
|
private dataHandler(res: SolrResponse, filterItem?: FacetItem): void {
|
||||||
console.log("dataHandlerSOLR (docs, numFound):");
|
// console.log("dataHandlerSOLR (docs, numFound):");
|
||||||
console.log(res.response.docs);
|
// console.log(res.response.docs);
|
||||||
console.log(res.response.numFound);
|
// console.log(res.response.numFound);
|
||||||
|
|
||||||
// Update results
|
// Update results
|
||||||
this.results = res.response.docs;
|
this.results = res.response.docs;
|
||||||
|
@ -242,6 +255,8 @@ export default class SearchViewComponent extends Vue {
|
||||||
|
|
||||||
// Method to handle pagination
|
// Method to handle pagination
|
||||||
onMenuClick(page: number) {
|
onMenuClick(page: number) {
|
||||||
|
console.log("onMenuClick");
|
||||||
|
|
||||||
this.pagination.currentPage = page;
|
this.pagination.currentPage = page;
|
||||||
const start = page * this.pagination.perPage - this.pagination.perPage;
|
const start = page * this.pagination.perPage - this.pagination.perPage;
|
||||||
|
|
||||||
|
@ -259,14 +274,19 @@ export default class SearchViewComponent extends Vue {
|
||||||
|
|
||||||
// Method to handle facet filtering
|
// Method to handle facet filtering
|
||||||
onFilter(facetItem: FacetItem): void {
|
onFilter(facetItem: FacetItem): void {
|
||||||
|
console.log("onFilter");
|
||||||
|
|
||||||
// Reset current page
|
// Reset current page
|
||||||
this.pagination.currentPage = 1;
|
this.pagination.currentPage = 1;
|
||||||
// console.log(facetItem.val);
|
console.log(facetItem.val);
|
||||||
|
console.log(facetItem.category);
|
||||||
|
|
||||||
// if (!this.activeFilterCategories.hasOwnProperty(facetItem.category)) {
|
// if (!this.activeFilterCategories.hasOwnProperty(facetItem.category)) {
|
||||||
|
|
||||||
// Check if filter item already exists
|
// Check if filter item already exists
|
||||||
if (!Object.prototype.hasOwnProperty.call(this.activeFilterCategories, facetItem.category)) {
|
if (!Object.prototype.hasOwnProperty.call(this.activeFilterCategories, facetItem.category)) {
|
||||||
this.activeFilterCategories[facetItem.category] = new Array<string>();
|
this.activeFilterCategories[facetItem.category] = new Array<string>();
|
||||||
|
console.log(this.activeFilterCategories);
|
||||||
}
|
}
|
||||||
// if (!this.activeFilterCategories[facetItem.category].some((e) => e === facetItem.val)) {
|
// if (!this.activeFilterCategories[facetItem.category].some((e) => e === facetItem.val)) {
|
||||||
|
|
||||||
|
@ -279,6 +299,7 @@ export default class SearchViewComponent extends Vue {
|
||||||
// (res: SolrResponse) => this.dataHandler(res, facetItem),
|
// (res: SolrResponse) => this.dataHandler(res, facetItem),
|
||||||
// (error: string) => this.errorHandler(error),
|
// (error: string) => this.errorHandler(error),
|
||||||
// );
|
// );
|
||||||
|
console.log(this.activeFilterCategories);
|
||||||
DatasetService.facetedSearchOPEN(this.searchTerm, this.activeFilterCategories, this.open.core, this.open.host, undefined).subscribe({
|
DatasetService.facetedSearchOPEN(this.searchTerm, this.activeFilterCategories, this.open.core, this.open.host, undefined).subscribe({
|
||||||
next: (res: OpenSearchResponse) => this.dataHandlerOPEN(res, facetItem),
|
next: (res: OpenSearchResponse) => this.dataHandlerOPEN(res, facetItem),
|
||||||
error: (error: string) => this.errorHandler(error),
|
error: (error: string) => this.errorHandler(error),
|
||||||
|
@ -286,60 +307,63 @@ export default class SearchViewComponent extends Vue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Method to clear facet category filter
|
// // // Method to clear facet category filter
|
||||||
onClearFacetCategory(categoryName: string): void {
|
// onClearFacetCategory(categoryName: string): void {
|
||||||
delete this.activeFilterCategories[categoryName];
|
// console.log("onClearFacetCategory");
|
||||||
|
|
||||||
|
// delete this.activeFilterCategories[categoryName];
|
||||||
|
|
||||||
// Trigger new search with updated filter
|
// // Trigger new search with updated filter
|
||||||
DatasetService.facetedSearch(this.searchTerm, this.activeFilterCategories, this.solr.core, this.solr.host, undefined).subscribe({
|
// DatasetService.facetedSearch(this.searchTerm, this.activeFilterCategories, this.solr.core, this.solr.host, undefined).subscribe({
|
||||||
next: (res: SolrResponse) => {
|
// next: (res: SolrResponse) => {
|
||||||
this.results = res.response.docs;
|
// this.results = res.response.docs;
|
||||||
this.numFound = res.response.numFound;
|
// this.numFound = res.response.numFound;
|
||||||
|
|
||||||
// pagination
|
// // pagination
|
||||||
this.pagination["total"] = res.response.numFound;
|
// this.pagination["total"] = res.response.numFound;
|
||||||
this.pagination["perPage"] = res.responseHeader.params.rows as number;
|
// this.pagination["perPage"] = res.responseHeader.params.rows as number;
|
||||||
this.pagination["currentPage"] = 1;
|
// this.pagination["currentPage"] = 1;
|
||||||
this.pagination["data"] = res.response.docs;
|
// this.pagination["data"] = res.response.docs;
|
||||||
|
|
||||||
const facet_fields: FacetFields = res.facets;
|
// const facet_fields: FacetFields = res.facets;
|
||||||
let prop: keyof typeof facet_fields;
|
// let prop: keyof typeof facet_fields;
|
||||||
for (prop in facet_fields) {
|
// for (prop in facet_fields) {
|
||||||
const facetCategory: FacetInstance = facet_fields[prop];
|
// const facetCategory: FacetInstance = facet_fields[prop];
|
||||||
if (facetCategory.buckets) {
|
// if (facetCategory.buckets) {
|
||||||
const facetItems: Array<FacetItem> = facetCategory.buckets;
|
// const facetItems: Array<FacetItem> = facetCategory.buckets;
|
||||||
|
|
||||||
const facetValues = facetItems.map((facetItem) => {
|
// const facetValues = facetItems.map((facetItem) => {
|
||||||
let rObj: FacetItem;
|
// let rObj: FacetItem;
|
||||||
if (this.facets[prop]?.some((e) => e.val === facetItem.val)) {
|
// if (this.facets[prop]?.some((e) => e.val === facetItem.val)) {
|
||||||
// console.log(facetValue + " is included")
|
// // console.log(facetValue + " is included")
|
||||||
// Update existing facet item with new count
|
// // Update existing facet item with new count
|
||||||
const indexOfFacetValue = this.facets[prop].findIndex((i) => i.val === facetItem.val);
|
// const indexOfFacetValue = this.facets[prop].findIndex((i) => i.val === facetItem.val);
|
||||||
// console.log(indexOfFacetValue);
|
// // console.log(indexOfFacetValue);
|
||||||
rObj = this.facets[prop][indexOfFacetValue];
|
// rObj = this.facets[prop][indexOfFacetValue];
|
||||||
rObj.count = facetItem.count;
|
// rObj.count = facetItem.count;
|
||||||
// rObj = new FacetItem(val, count);
|
// // rObj = new FacetItem(val, count);
|
||||||
// if facet ccategory is reactivated category, deactivate all filter items
|
// // if facet ccategory is reactivated category, deactivate all filter items
|
||||||
if (prop == categoryName) {
|
// if (prop == categoryName) {
|
||||||
rObj.active = false;
|
// rObj.active = false;
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
// Create new facet item
|
// // Create new facet item
|
||||||
rObj = new FacetItem(facetItem.val, facetItem.count);
|
// rObj = new FacetItem(facetItem.val, facetItem.count);
|
||||||
}
|
// }
|
||||||
return rObj;
|
// return rObj;
|
||||||
});
|
// });
|
||||||
this.facets[prop] = facetValues;
|
// this.facets[prop] = facetValues;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
error: (error: string) => this.errorHandler(error),
|
// error: (error: string) => this.errorHandler(error),
|
||||||
complete: () => console.log("clear facet category completed"),
|
// complete: () => console.log("clear facet category completed"),
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Method to clear facet category filter
|
// Method to clear facet category filter
|
||||||
onClearFacetCategoryOPEN(categoryName: string): void {
|
onClearFacetCategoryOPEN(categoryName: string): void {
|
||||||
|
console.log("onClearFacetCategory");
|
||||||
delete this.activeFilterCategories[categoryName];
|
delete this.activeFilterCategories[categoryName];
|
||||||
|
|
||||||
// Trigger new search with updated filter
|
// Trigger new search with updated filter
|
||||||
|
|
Loading…
Reference in New Issue
Block a user