forked from MapComplete/MapComplete
Feature(search): add error message if search failed
This commit is contained in:
parent
20984d1a46
commit
0cbfa325f6
11 changed files with 138 additions and 50 deletions
|
@ -7,6 +7,11 @@ export default class CombinedSearcher implements GeocodingProvider {
|
||||||
private _providers: ReadonlyArray<GeocodingProvider>
|
private _providers: ReadonlyArray<GeocodingProvider>
|
||||||
private _providersWithSuggest: ReadonlyArray<GeocodingProvider>
|
private _providersWithSuggest: ReadonlyArray<GeocodingProvider>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges the various providers together; ignores errors.
|
||||||
|
* IF all providers fail, no errors will be given
|
||||||
|
* @param providers
|
||||||
|
*/
|
||||||
constructor(...providers: ReadonlyArray<GeocodingProvider>) {
|
constructor(...providers: ReadonlyArray<GeocodingProvider>) {
|
||||||
this._providers = Utils.NoNull(providers)
|
this._providers = Utils.NoNull(providers)
|
||||||
this._providersWithSuggest = this._providers.filter((pr) => pr.suggest !== undefined)
|
this._providersWithSuggest = this._providers.filter((pr) => pr.suggest !== undefined)
|
||||||
|
@ -45,9 +50,10 @@ export default class CombinedSearcher implements GeocodingProvider {
|
||||||
return CombinedSearcher.merge(results)
|
return CombinedSearcher.merge(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
suggest(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
|
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]}> {
|
||||||
return Stores.concat(
|
const concatted = Stores.concat(
|
||||||
this._providersWithSuggest.map((pr) => pr.suggest(query, options))
|
this._providersWithSuggest.map((pr) => <Store<GeocodeResult[]>> pr.suggest(query, options).map(result => result["success"] ?? []))
|
||||||
).map((gcrss) => CombinedSearcher.merge(gcrss))
|
);
|
||||||
|
return concatted.map(gcrss => ({success: CombinedSearcher.merge(gcrss) }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,8 +119,8 @@ export default class CoordinateSearch implements GeocodingProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suggest(query: string): Store<GeocodeResult[]> {
|
suggest(query: string): Store<{success: GeocodeResult[]}> {
|
||||||
return new ImmutableStore(this.directSearch(query))
|
return new ImmutableStore({success: this.directSearch(query)})
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query: string): Promise<GeocodeResult[]> {
|
async search(query: string): Promise<GeocodeResult[]> {
|
||||||
|
|
|
@ -57,11 +57,7 @@ export default interface GeocodingProvider {
|
||||||
*/
|
*/
|
||||||
search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]>
|
search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]>
|
||||||
|
|
||||||
/**
|
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error :any}>
|
||||||
* @param query
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
suggest?(query: string, options?: GeocodingOptions): Store<GeocodeResult[]>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReverseGeocodingResult = Feature<
|
export type ReverseGeocodingResult = Feature<
|
||||||
|
@ -93,7 +89,7 @@ export class GeocodingUtils {
|
||||||
// We are resetting the layeroverview; trying to parse is useless
|
// We are resetting the layeroverview; trying to parse is useless
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return new LayerConfig(<LayerConfigJson>search, "search")
|
return new LayerConfig(<LayerConfigJson><any> search, "search")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static categoryToZoomLevel: Record<GeocodingCategory, number> = {
|
public static categoryToZoomLevel: Record<GeocodingCategory, number> = {
|
||||||
|
|
|
@ -141,7 +141,7 @@ export default class LocalElementSearch implements GeocodingProvider {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
suggest(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
|
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]}> {
|
||||||
return this.searchEntries(query, options, true)
|
return this.searchEntries(query, options, true).mapD(r => ({success:r}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Constants from "../../Models/Constants"
|
||||||
import { FeatureCollection } from "geojson"
|
import { FeatureCollection } from "geojson"
|
||||||
import Locale from "../../UI/i18n/Locale"
|
import Locale from "../../UI/i18n/Locale"
|
||||||
import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider"
|
import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||||
|
import { Store, UIEventSource } from "../UIEventSource"
|
||||||
|
|
||||||
export class NominatimGeocoding implements GeocodingProvider {
|
export class NominatimGeocoding implements GeocodingProvider {
|
||||||
private readonly _host
|
private readonly _host
|
||||||
|
@ -37,4 +38,8 @@ export class NominatimGeocoding implements GeocodingProvider {
|
||||||
}&zoom=${Math.ceil(zoom) + 1}&accept-language=${language}`
|
}&zoom=${Math.ceil(zoom) + 1}&accept-language=${language}`
|
||||||
return Utils.downloadJson(url)
|
return Utils.downloadJson(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suggest(query: string, options?: GeocodingOptions): Store<{ success: GeocodeResult[] } | { error: any }> {
|
||||||
|
return UIEventSource.FromPromiseWithErr(this.search(query, options))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Store, Stores } from "../UIEventSource"
|
import { ImmutableStore, Store } from "../UIEventSource"
|
||||||
import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider"
|
import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||||
import { decode as pluscode_decode } from "pluscodes"
|
import { decode as pluscode_decode } from "pluscodes"
|
||||||
|
|
||||||
|
@ -24,24 +24,30 @@ export default class OpenLocationCodeSearch implements GeocodingProvider {
|
||||||
return str.toUpperCase().match(this._isPlusCode) !== null
|
return str.toUpperCase().match(this._isPlusCode) !== null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private searchDirectly(query: string): GeocodeResult | undefined {
|
||||||
|
if (!OpenLocationCodeSearch.isPlusCode(query)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const { latitude, longitude } = pluscode_decode(query)
|
||||||
|
return {
|
||||||
|
lon: longitude,
|
||||||
|
lat: latitude,
|
||||||
|
description: "Open Location Code",
|
||||||
|
osm_id: query,
|
||||||
|
display_name: query.toUpperCase(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> {
|
async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> {
|
||||||
if (!OpenLocationCodeSearch.isPlusCode(query)) {
|
if (!OpenLocationCodeSearch.isPlusCode(query)) {
|
||||||
return [] // Must be an empty list and not "undefined", the latter is interpreted as 'still searching'
|
return [] // Must be an empty list and not "undefined", the latter is interpreted as 'still searching'
|
||||||
}
|
}
|
||||||
const { latitude, longitude } = pluscode_decode(query)
|
|
||||||
|
|
||||||
return [
|
return [this.searchDirectly(query)]
|
||||||
{
|
|
||||||
lon: longitude,
|
|
||||||
lat: latitude,
|
|
||||||
description: "Open Location Code",
|
|
||||||
osm_id: query,
|
|
||||||
display_name: query.toUpperCase(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suggest?(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
|
suggest(query: string, options?: GeocodingOptions): Store<{ success: GeocodeResult[] }> {
|
||||||
return Stores.FromPromise(this.search(query, options))
|
const result = OpenLocationCodeSearch.isPlusCode(query) ? [this.searchDirectly(query)] : []
|
||||||
|
return new ImmutableStore({ success: result })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
|
||||||
return [await this.getInfoAbout(id)]
|
return [await this.getInfoAbout(id)]
|
||||||
}
|
}
|
||||||
|
|
||||||
suggest?(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
|
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error: any}> {
|
||||||
return UIEventSource.FromPromise(this.search(query, options))
|
return UIEventSource.FromPromiseWithErr(this.search(query, options))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,10 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
||||||
this.suggestionLimit = suggestionLimit
|
this.suggestionLimit = suggestionLimit
|
||||||
this.searchLimit = searchLimit
|
this.searchLimit = searchLimit
|
||||||
this._endpoint = endpoint ?? Constants.photonEndpoint ?? "https://photon.komoot.io/"
|
this._endpoint = endpoint ?? Constants.photonEndpoint ?? "https://photon.komoot.io/"
|
||||||
|
|
||||||
|
if(this.ignoreBounds){
|
||||||
|
this.name += " (global)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async reverseSearch(
|
async reverseSearch(
|
||||||
|
@ -69,8 +73,8 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
||||||
return `&lang=${language}`
|
return `&lang=${language}`
|
||||||
}
|
}
|
||||||
|
|
||||||
suggest(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
|
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error: any}> {
|
||||||
return Stores.FromPromise(this.search(query, options))
|
return Stores.FromPromiseWithErr(this.search(query, options))
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildDescription(entry: Feature) {
|
private buildDescription(entry: Feature) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import GeocodingProvider, { GeocodeResult, GeocodingUtils } from "../Search/GeocodingProvider"
|
import GeocodingProvider, { GeocodeResult, GeocodingUtils } from "../Search/GeocodingProvider"
|
||||||
import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource"
|
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||||
import CombinedSearcher from "../Search/CombinedSearcher"
|
import CombinedSearcher from "../Search/CombinedSearcher"
|
||||||
import FilterSearch, { FilterSearchResult } from "../Search/FilterSearch"
|
import FilterSearch, { FilterSearchResult } from "../Search/FilterSearch"
|
||||||
import LocalElementSearch from "../Search/LocalElementSearch"
|
import LocalElementSearch from "../Search/LocalElementSearch"
|
||||||
|
@ -18,6 +18,8 @@ import { Feature } from "geojson"
|
||||||
import OpenLocationCodeSearch from "../Search/OpenLocationCodeSearch"
|
import OpenLocationCodeSearch from "../Search/OpenLocationCodeSearch"
|
||||||
import { BBox } from "../BBox"
|
import { BBox } from "../BBox"
|
||||||
import { QueryParameters } from "../Web/QueryParameters"
|
import { QueryParameters } from "../Web/QueryParameters"
|
||||||
|
import { Utils } from "../../Utils"
|
||||||
|
import { NominatimGeocoding } from "../Search/NominatimGeocoding"
|
||||||
|
|
||||||
export default class SearchState {
|
export default class SearchState {
|
||||||
public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
|
public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
|
||||||
|
@ -32,7 +34,12 @@ export default class SearchState {
|
||||||
private readonly state: ThemeViewState
|
private readonly state: ThemeViewState
|
||||||
public readonly showSearchDrawer: UIEventSource<boolean>
|
public readonly showSearchDrawer: UIEventSource<boolean>
|
||||||
public readonly suggestionsSearchRunning: Store<boolean>
|
public readonly suggestionsSearchRunning: Store<boolean>
|
||||||
|
public readonly runningEngines: Store<GeocodingProvider[]>
|
||||||
public readonly locationResults: FeatureSource
|
public readonly locationResults: FeatureSource
|
||||||
|
/**
|
||||||
|
* Indicates failures in the current search
|
||||||
|
*/
|
||||||
|
public readonly failedEngines: Store<{ source: GeocodingProvider; error: any }[]>
|
||||||
|
|
||||||
constructor(state: ThemeViewState) {
|
constructor(state: ThemeViewState) {
|
||||||
this.state = state
|
this.state = state
|
||||||
|
@ -44,31 +51,63 @@ export default class SearchState {
|
||||||
new CoordinateSearch(),
|
new CoordinateSearch(),
|
||||||
new OpenLocationCodeSearch(),
|
new OpenLocationCodeSearch(),
|
||||||
new OpenStreetMapIdSearch(state.osmObjectDownloader),
|
new OpenStreetMapIdSearch(state.osmObjectDownloader),
|
||||||
new PhotonSearch(true, 2),
|
new PhotonSearch(true, 2), // global results
|
||||||
new PhotonSearch(),
|
new PhotonSearch(), // local results
|
||||||
// new NominatimGeocoding(),
|
new NominatimGeocoding(),
|
||||||
]
|
]
|
||||||
|
|
||||||
const bounds = state.mapProperties.bounds
|
const bounds = state.mapProperties.bounds
|
||||||
const suggestionsList = this.searchTerm.stabilized(250).mapD(
|
const suggestionsListWithSource = this.searchTerm.stabilized(250).mapD(
|
||||||
(search) => {
|
(search) => {
|
||||||
if (search.length === 0) {
|
if (search.length === 0) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return this.locationSearchers.map((ls) => ls.suggest(search, { bbox: bounds.data }))
|
return this.locationSearchers.map((ls) => ({
|
||||||
|
source: ls,
|
||||||
|
results: ls.suggest(search, { bbox: bounds.data }),
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
[bounds]
|
[bounds]
|
||||||
)
|
)
|
||||||
this.suggestionsSearchRunning = suggestionsList.bind((suggestions) => {
|
const suggestionsList = suggestionsListWithSource
|
||||||
if (suggestions === undefined) {
|
.mapD(list => list.map(sugg => sugg.results))
|
||||||
return new ImmutableStore(true)
|
|
||||||
}
|
const isRunningPerEngine: Store<Store<GeocodingProvider>[]> =
|
||||||
return Stores.concat(suggestions).map((suggestions) =>
|
suggestionsListWithSource.map(
|
||||||
suggestions.some((list) => list === undefined)
|
allProviders => allProviders.map(provider =>
|
||||||
)
|
provider.results.map(result => {
|
||||||
})
|
if (result === undefined) {
|
||||||
|
return provider.source
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
this.runningEngines = isRunningPerEngine.bindD(
|
||||||
|
listOfSources => Stores.concat(listOfSources).mapD(list => Utils.NoNull(list)))
|
||||||
|
|
||||||
|
|
||||||
|
this.failedEngines = suggestionsListWithSource
|
||||||
|
.bindD((allProviders: {
|
||||||
|
source: GeocodingProvider;
|
||||||
|
results: Store<{ success: GeocodeResult[] } | { error: any }>
|
||||||
|
}[]) => Stores.concat(
|
||||||
|
allProviders.map(providerAndResult =>
|
||||||
|
<Store<{ source: GeocodingProvider, error: any }[]>>providerAndResult.results.map(result => {
|
||||||
|
let error = result?.["error"]
|
||||||
|
if (error) {
|
||||||
|
return [{
|
||||||
|
source: providerAndResult.source, error,
|
||||||
|
}]
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
))).map(list => Utils.NoNull(list.flatMap(x => x)))
|
||||||
|
|
||||||
|
this.suggestionsSearchRunning = this.runningEngines.map(running => running?.length > 0)
|
||||||
this.suggestions = suggestionsList.bindD((suggestions) =>
|
this.suggestions = suggestionsList.bindD((suggestions) =>
|
||||||
Stores.concat(suggestions).map((suggestions) => CombinedSearcher.merge(suggestions))
|
Stores.concat(suggestions.map(sugg => sugg.map(maybe => maybe?.["success"])))
|
||||||
|
.map((suggestions: GeocodeResult[][]) => CombinedSearcher.merge(suggestions))
|
||||||
)
|
)
|
||||||
|
|
||||||
const themeSearch = ThemeSearchIndex.fromState(state)
|
const themeSearch = ThemeSearchIndex.fromState(state)
|
||||||
|
|
|
@ -45,15 +45,16 @@ export class Stores {
|
||||||
?.then((d) => src.setData(d))
|
?.then((d) => src.setData(d))
|
||||||
return src
|
return src
|
||||||
}
|
}
|
||||||
|
public static concat<T>(stores: Store<T | undefined>[]): Store<(T | undefined)[]> ;
|
||||||
public static concat<T>(stores: Store<T[] | undefined>[]): Store<(T[] | undefined)[]> {
|
public static concat<T>(stores: Store<T>[]): Store<T[]> ;
|
||||||
const newStore = new UIEventSource<(T[] | undefined)[]>([])
|
public static concat<T>(stores: Store<T | undefined>[]): Store<(T | undefined)[]> {
|
||||||
|
const newStore = new UIEventSource<(T | undefined)[]>([])
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
if (newStore._callbacks.isDestroyed) {
|
if (newStore._callbacks.isDestroyed) {
|
||||||
return true // unregister
|
return true // unregister
|
||||||
}
|
}
|
||||||
const results: (T[] | undefined)[] = []
|
const results: (T | undefined)[] = []
|
||||||
for (const store of stores) {
|
for (const store of stores) {
|
||||||
results.push(store.data)
|
results.push(store.data)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,22 +8,36 @@
|
||||||
import { default as GeocodeResultSvelte } from "./GeocodeResult.svelte"
|
import { default as GeocodeResultSvelte } from "./GeocodeResult.svelte"
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte"
|
||||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
|
import type GeocodingProvider from "../../Logic/Search/GeocodingProvider"
|
||||||
import type { GeocodeResult } from "../../Logic/Search/GeocodingProvider"
|
import type { GeocodeResult } from "../../Logic/Search/GeocodingProvider"
|
||||||
import type { MapProperties } from "../../Models/MapProperties"
|
import type { MapProperties } from "../../Models/MapProperties"
|
||||||
|
import { ExclamationTriangle } from "@babeard/svelte-heroicons/solid/ExclamationTriangle"
|
||||||
|
|
||||||
export let state: {
|
export let state: {
|
||||||
searchState: {
|
searchState: {
|
||||||
searchTerm: UIEventSource<string>
|
searchTerm: UIEventSource<string>
|
||||||
suggestions: Store<GeocodeResult[]>
|
suggestions: Store<GeocodeResult[]>
|
||||||
suggestionsSearchRunning: Store<boolean>
|
suggestionsSearchRunning: Store<boolean>
|
||||||
|
runningEngines: Store<string[]>
|
||||||
|
failedEngines: Store<{
|
||||||
|
source: GeocodingProvider;
|
||||||
|
error: any
|
||||||
|
}[]>
|
||||||
}
|
}
|
||||||
|
featureSwitchIsTesting?: Store<boolean>
|
||||||
|
userRelatedState?: { showTagsB: Store<boolean> }
|
||||||
mapProperties: MapProperties
|
mapProperties: MapProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
let searchTerm = state.searchState.searchTerm
|
let searchTerm = state.searchState.searchTerm
|
||||||
let results = state.searchState.suggestions
|
let results = state.searchState.suggestions
|
||||||
let isSearching = state.searchState.suggestionsSearchRunning ?? new ImmutableStore(false)
|
let isSearching = state.searchState.suggestionsSearchRunning ?? new ImmutableStore(false)
|
||||||
|
let runningEngines = state.searchState.runningEngines ?? new ImmutableStore([])
|
||||||
|
let failedEngines = state.searchState.failedEngines
|
||||||
|
|
||||||
|
|
||||||
|
let isTesting = state.featureSwitchIsTesting ?? new ImmutableStore(false)
|
||||||
|
let showTags = state.userRelatedState?.showTagsB ?? new ImmutableStore(false)
|
||||||
const t = Translations.t.general.search
|
const t = Translations.t.general.search
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -43,6 +57,11 @@
|
||||||
<div class="m-4 my-8 flex justify-center">
|
<div class="m-4 my-8 flex justify-center">
|
||||||
<Loading>
|
<Loading>
|
||||||
<Tr t={t.searching} />
|
<Tr t={t.searching} />
|
||||||
|
{#if $isTesting || $showTags}
|
||||||
|
<div class="subtle">
|
||||||
|
Querying {$runningEngines?.map(provider => provider.name)?.join(", ")}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</Loading>
|
</Loading>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -52,6 +71,18 @@
|
||||||
<Tr t={t.nothingFor.Subs({ term: "<i>" + $searchTerm + "</i>" })} />
|
<Tr t={t.nothingFor.Subs({ term: "<i>" + $searchTerm + "</i>" })} />
|
||||||
</b>
|
</b>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $failedEngines?.length > 0}
|
||||||
|
{#each $failedEngines as failed}
|
||||||
|
<div class="warning flex flex-col items-center py-4 overflow-hidden">
|
||||||
|
<ExclamationTriangle class="w-4" />
|
||||||
|
Search using {failed.source.name} failed
|
||||||
|
<div class="subtle text-sm">
|
||||||
|
{failed.error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
</SidebarUnit>
|
</SidebarUnit>
|
||||||
{:else}
|
{:else}
|
||||||
<slot name="if-no-results" />
|
<slot name="if-no-results" />
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue