From 48186aa53098074183861a4e40fd2e6eb860c147 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 17 Sep 2024 02:16:25 +0200 Subject: [PATCH] Search: similar filters in different layers are now merged, fix # --- .../2024-09-11 Usertest search Jewelry.md | 2 +- langs/en.json | 1 + langs/layers/cs.json | 2 +- langs/layers/de.json | 2 +- langs/layers/en.json | 2 +- public/css/index-tailwind-output.css | 21 +++++++--- src/Logic/Search/FilterSearch.ts | 20 ++++++++- src/Logic/State/SearchState.ts | 36 +++++++++------- src/UI/Base/SidebarUnit.svelte | 1 + src/UI/Flowbite/AccordionSingle.svelte | 9 +++- src/UI/Search/ActiveFilter.svelte | 19 +++++++-- src/UI/Search/ActiveFilters.svelte | 19 +++++---- src/UI/Search/FilterResult.svelte | 23 ++++++---- src/UI/Search/FilterResults.svelte | 42 ++++++++++++++----- src/UI/Search/SearchResults.svelte | 20 ++++++--- 15 files changed, 156 insertions(+), 63 deletions(-) diff --git a/Docs/UserTests/2024-09-11 Usertest search Jewelry.md b/Docs/UserTests/2024-09-11 Usertest search Jewelry.md index 0a39f707df..29d27b919a 100644 --- a/Docs/UserTests/2024-09-11 Usertest search Jewelry.md +++ b/Docs/UserTests/2024-09-11 Usertest search Jewelry.md @@ -26,5 +26,5 @@ To validate the 'search with filters', the tester was tasked with searching all ## To improve -[ ] Why are there multiple "Open Now" filters? +[x] Why are there multiple "Open Now" filters? Because of different layers with a similar filter, they are now shown merged [x] Special layers (e.g. gps location) are disabled as well (fixed now) diff --git a/langs/en.json b/langs/en.json index 975312a76e..2d71a1b480 100644 --- a/langs/en.json +++ b/langs/en.json @@ -406,6 +406,7 @@ "error": "Something went wrong…", "instructions": "Use the search bar above to search for locations, filters or other thematic maps", "locations": "Locations", + "nMoreFilters": "{n} more", "nothing": "Nothing found…", "nothingFor": "No results found for {term}", "otherMaps": "Other maps", diff --git a/langs/layers/cs.json b/langs/layers/cs.json index 0cd1ede278..24028a933e 100644 --- a/langs/layers/cs.json +++ b/langs/layers/cs.json @@ -7208,7 +7208,7 @@ }, "description": "Obchod", "filter": { - "1": { + "0": { "options": { "0": { "question": "Zobrazit pouze obchody prodávající použité zboží" diff --git a/langs/layers/de.json b/langs/layers/de.json index 800e6f8a4c..ab106ec9f0 100644 --- a/langs/layers/de.json +++ b/langs/layers/de.json @@ -9087,7 +9087,7 @@ }, "description": "Ein Geschäft", "filter": { - "1": { + "0": { "options": { "0": { "question": "Nur Second-Hand-Geschäfte anzeigen" diff --git a/langs/layers/en.json b/langs/layers/en.json index ea04a61c77..456707523c 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -9122,7 +9122,7 @@ }, "description": "A shop", "filter": { - "1": { + "0": { "options": { "0": { "question": "Only show shops selling second-hand items" diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 46312654ba..733b5ddd15 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -1188,14 +1188,14 @@ input[type="range"].range-lg::-moz-range-thumb { left: 25%; } -.bottom-4 { - bottom: 1rem; -} - .top-6 { top: 1.5rem; } +.bottom-4 { + bottom: 1rem; +} + .bottom-5 { bottom: 1.25rem; } @@ -2760,6 +2760,10 @@ input[type="range"].range-lg::-moz-range-thumb { overflow-y: auto; } +.overflow-x-hidden { + overflow-x: hidden; +} + .overflow-y-scroll { overflow-y: scroll; } @@ -4719,6 +4723,11 @@ input[type="range"].range-lg::-moz-range-thumb { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } +.shadow-transparent { + --tw-shadow-color: transparent; + --tw-shadow: var(--tw-shadow-colored); +} + .shadow-gray-300 { --tw-shadow-color: #D1D5DB; --tw-shadow: var(--tw-shadow-colored); @@ -5353,8 +5362,8 @@ h2.group { align-items: center; white-space: nowrap; border-radius: 999rem; - padding-left: 0.5rem; - padding-right: 0.5rem; + padding-left: 0.25rem; + padding-right: 0.25rem; border: 1px solid var(--subtle-detail-color-light-contrast); background-color: var(--low-interaction-background); } diff --git a/src/Logic/Search/FilterSearch.ts b/src/Logic/Search/FilterSearch.ts index 37a0c05b39..a08b0bb5c9 100644 --- a/src/Logic/Search/FilterSearch.ts +++ b/src/Logic/Search/FilterSearch.ts @@ -1,4 +1,3 @@ -import { SpecialVisualizationState } from "../../UI/SpecialVisualization" import { Utils } from "../../Utils" import Locale from "../../UI/i18n/Locale" import Constants from "../../Models/Constants" @@ -110,4 +109,23 @@ export default class FilterSearch { Utils.shuffle(result) return result.slice(0, 6) } + + /** + * Partitions the list of filters in such a way that identically appearing filters will be in the same sublist. + * + * Note that this depends on the language and the displayed text. For example, two filters {"en": "A", "nl": "B"} and {"en": "X", "nl": "B"} will be joined for dutch but not for English + * + */ + static mergeSemiIdenticalLayers(filters: ReadonlyArray, language: string):T[][] { + const results : Record = {} + for (const filter of filters) { + const txt = filter.option.question.textFor(language) + if(results[txt]){ + results[txt].push(filter) + }else{ + results[txt] = [filter] + } + } + return Object.values(results) + } } diff --git a/src/Logic/State/SearchState.ts b/src/Logic/State/SearchState.ts index cc3e5c1a66..4e26215f29 100644 --- a/src/Logic/State/SearchState.ts +++ b/src/Logic/State/SearchState.ts @@ -79,7 +79,7 @@ export default class SearchState { return !foundMatch }) }, [state.layerState.activeFilters]) - this.locationResults =new GeocodingFeatureSource(this.suggestions.stabilized(250)) + this.locationResults = new GeocodingFeatureSource(this.suggestions.stabilized(250)) this.showSearchDrawer = new UIEventSource(false) @@ -92,7 +92,7 @@ export default class SearchState { } - public async apply(result: FilterSearchResult | LayerConfig) { + public async apply(result: FilterSearchResult[] | LayerConfig) { if (result instanceof LayerConfig) { return this.applyLayer(result) } @@ -105,29 +105,35 @@ export default class SearchState { } } - private async applyFilter(payload: FilterSearchResult) { + private async applyFilter(payload: FilterSearchResult[]) { const state = this.state - const { layer, filter, index } = payload + const layers = payload.map(fsr => fsr.layer.id) for (const [name, otherLayer] of state.layerState.filteredLayers) { const layer = otherLayer.layerDef - if(!layer.isNormal()){ + if (!layer.isNormal()) { continue } - otherLayer.isDisplayed.setData(payload.layer.id === layer.id) + if(otherLayer.layerDef.minzoom > state.mapProperties.minzoom.data) { + // Currently not displayed, we don't hide + continue + } + otherLayer.isDisplayed.setData(layers.indexOf(layer.id) > 0) } - const flayer = state.layerState.filteredLayers.get(layer.id) - flayer.isDisplayed.set(true) - const filtercontrol = flayer.appliedFilters.get(filter.id) - if (filtercontrol.data === index) { - filtercontrol.setData(undefined) - } else { - filtercontrol.setData(index) + for (const { filter, index, layer } of payload) { + const flayer = state.layerState.filteredLayers.get(layer.id) + flayer.isDisplayed.set(true) + const filtercontrol = flayer.appliedFilters.get(filter.id) + if (filtercontrol.data === index) { + filtercontrol.setData(undefined) + } else { + filtercontrol.setData(index) + } } } closeIfFullscreen() { - if(window.innerWidth < 640){ + if (window.innerWidth < 640) { this.showSearchDrawer.set(false) } } @@ -135,7 +141,7 @@ export default class SearchState { clickedOnMap(feature: Feature) { const osmid = feature.properties.osm_id const localElement = this.state.indexedFeatures.featuresById.data.get(osmid) - if(localElement){ + if (localElement) { this.state.selectedElement.set(localElement) return } diff --git a/src/UI/Base/SidebarUnit.svelte b/src/UI/Base/SidebarUnit.svelte index aa1b8fcdca..8ddd521240 100644 --- a/src/UI/Base/SidebarUnit.svelte +++ b/src/UI/Base/SidebarUnit.svelte @@ -10,6 +10,7 @@ background: var(--background-color); padding: 0.5rem; border-radius: 0.5rem; + overflow-y: auto; } :global(.sidebar-unit > h3) { diff --git a/src/UI/Flowbite/AccordionSingle.svelte b/src/UI/Flowbite/AccordionSingle.svelte index 5857cb1e9b..e57fadb4f1 100644 --- a/src/UI/Flowbite/AccordionSingle.svelte +++ b/src/UI/Flowbite/AccordionSingle.svelte @@ -2,11 +2,16 @@ import { Accordion, AccordionItem } from "flowbite-svelte" export let expanded = false + export let noBorder = false +let defaultClass: string = undefined + if(noBorder){ + defaultClass = "unstyled w-full flex-grow" + } - - + +
diff --git a/src/UI/Search/ActiveFilter.svelte b/src/UI/Search/ActiveFilter.svelte index 78ea31b569..0ef66bf696 100644 --- a/src/UI/Search/ActiveFilter.svelte +++ b/src/UI/Search/ActiveFilter.svelte @@ -3,25 +3,36 @@ import FilterOption from "./FilterOption.svelte" import Loading from "../Base/Loading.svelte" import FilterToggle from "./FilterToggle.svelte" + import type { SpecialVisualizationState } from "../SpecialVisualization" - export let activeFilter: ActiveFilter - let { control, filter } = activeFilter + export let activeFilter: ActiveFilter[] + let { control, filter } = activeFilter[0] let option = control.map(c => filter.options[c] ?? filter.options[0]) let loading = false function clear() { loading = true requestIdleCallback(() => { - control.setData(undefined) + for (const af of activeFilter) { + af.control.setData(undefined) + } loading = false }) } + + export let state: SpecialVisualizationState + let debug = state.featureSwitches.featureSwitchIsDebugging {#if loading} {:else } - clear()}> + clear()}> + {#if $debug} + + ({activeFilter.map(af => af.layer.id).join(", ")}) + + {/if} {/if} diff --git a/src/UI/Search/ActiveFilters.svelte b/src/UI/Search/ActiveFilters.svelte index 8147628e22..be4d27c98b 100644 --- a/src/UI/Search/ActiveFilters.svelte +++ b/src/UI/Search/ActiveFilters.svelte @@ -8,10 +8,17 @@ import FilterToggle from "./FilterToggle.svelte" import ToSvelte from "../Base/ToSvelte.svelte" import Tr from "../Base/Tr.svelte" - import { Store } from "../../Logic/UIEventSource" + import { Store, UIEventSource } from "../../Logic/UIEventSource" import Translations from "../i18n/Translations" + import type { FilterSearchResult } from "../../Logic/Search/FilterSearch" + import FilterSearch from "../../Logic/Search/FilterSearch" - export let activeFilters: ActiveFilter[] + import Locale from "../i18n/Locale" + + export let activeFilters: ( FilterSearchResult & ActiveFilter)[] + let language = Locale.language + let mergedActiveFilters = FilterSearch.mergeSemiIdenticalLayers(activeFilters, $language) + $:mergedActiveFilters = FilterSearch.mergeSemiIdenticalLayers(activeFilters, $language) export let state: SpecialVisualizationState let loading = false const t =Translations.t.general.search @@ -33,8 +40,6 @@ loading = true requestIdleCallback(() => { enableAllLayers() - - for (const activeFilter of activeFilters) { activeFilter.control.setData(undefined) } @@ -44,7 +49,7 @@ } -{#if activeFilters.length > 0 || $nonactiveLayers.length > 0} +{#if mergedActiveFilters.length > 0 || $nonactiveLayers.length > 0}

@@ -81,9 +86,9 @@ {/if} - {#each activeFilters as activeFilter (activeFilter)} + {#each mergedActiveFilters as activeFilter (activeFilter)}
- +
{/each}
diff --git a/src/UI/Search/FilterResult.svelte b/src/UI/Search/FilterResult.svelte index 41d385daa3..803701a3ea 100644 --- a/src/UI/Search/FilterResult.svelte +++ b/src/UI/Search/FilterResult.svelte @@ -8,13 +8,19 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import Loading from "../Base/Loading.svelte" - export let entry: FilterSearchResult | LayerConfig - let isLayer = entry instanceof LayerConfig - let asLayer = entry - let asFilter = entry + export let entry: FilterSearchResult[] | LayerConfig + let asFilter: FilterSearchResult[] + let asLayer: LayerConfig + if(Array.isArray(entry)){ + asFilter = entry + }else{ + asLayer = entry + + } export let state: SpecialVisualizationState let loading = false + let debug = state.featureSwitches.featureSwitchIsDebugging function apply() { loading = true @@ -34,7 +40,7 @@ {/if}
- {#if isLayer} + {#if asLayer}
@@ -42,8 +48,11 @@ {:else} - - + + + {#if $debug} + ({asFilter.map(f => f.layer.id).join(", ")}) + {/if} {/if}
diff --git a/src/UI/Search/FilterResults.svelte b/src/UI/Search/FilterResults.svelte index cb5cb96c41..a300987788 100644 --- a/src/UI/Search/FilterResults.svelte +++ b/src/UI/Search/FilterResults.svelte @@ -4,15 +4,21 @@ import type { SpecialVisualizationState } from "../SpecialVisualization" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" + import FilterSearch from "../../Logic/Search/FilterSearch" import type { FilterSearchResult } from "../../Logic/Search/FilterSearch" + import Tr from "../Base/Tr.svelte" import Translations from "../i18n/Translations" + import Locale from "../i18n/Locale" + import { Store } from "../../Logic/UIEventSource" + import AccordionSingle from "../Flowbite/AccordionSingle.svelte" export let state: SpecialVisualizationState let searchTerm = state.searchState.searchTerm let activeLayers = state.layerState.activeLayers let filterResults = state.searchState.filterSuggestions + let filtersMerged = filterResults.map(filters => FilterSearch.mergeSemiIdenticalLayers(filters, Locale.language.data), [Locale.language]) let layerResults = state.searchState.layerSuggestions.map(layers => { const nowActive = activeLayers.data.filter(al => al.layerDef.isNormal()) @@ -22,30 +28,44 @@ } return layers }, [activeLayers]) - let filterResultsClipped = filterResults.mapD(filters => { + let filterResultsClipped: Store<{ + clipped: (FilterSearchResult[] | LayerConfig)[], + rest?: (FilterSearchResult[] | LayerConfig)[] + }> = filtersMerged.mapD(filters => { let layers = layerResults.data - const ls: (FilterSearchResult | LayerConfig)[] = [].concat(layers, filters) + const ls: (FilterSearchResult[] | LayerConfig)[] = [].concat(layers, filters) if (ls.length <= 6) { - return ls + return { clipped: ls } } - return ls.slice(0, 4) - }, [layerResults, activeLayers]) + return { clipped: ls.slice(0, 4), rest: ls.slice(4) } + }, [layerResults, activeLayers, Locale.language]) {#if $searchTerm.length > 0 && ($filterResults.length > 0 || $layerResults.length > 0)} -

+

+ +

- {#each $filterResultsClipped as filterResult (filterResult)} + {#each $filterResultsClipped.clipped as filterResult (filterResult)} {/each}
- {#if $filterResults.length + $layerResults.length > $filterResultsClipped.length} -
- ... and {$filterResults.length + $layerResults.length - $filterResultsClipped.length} more ... -
+ {#if $filtersMerged.length + $layerResults.length > $filterResultsClipped.clipped.length} + +
+ +
+
+ {#each $filterResultsClipped.rest as filterResult (filterResult)} + + {/each} +
+
{/if}
{/if} diff --git a/src/UI/Search/SearchResults.svelte b/src/UI/Search/SearchResults.svelte index 6bfdd481b8..067b69a384 100644 --- a/src/UI/Search/SearchResults.svelte +++ b/src/UI/Search/SearchResults.svelte @@ -4,16 +4,22 @@ import Constants from "../../Models/Constants" import type { ActiveFilter } from "../../Logic/State/LayerState" import ThemeViewState from "../../Models/ThemeViewState" - import ThemeResults from "./ThemeResults.svelte" + import ThemeResults from "./ThemeResults.svelte" import GeocodeResults from "./GeocodeResults.svelte" import FilterResults from "./FilterResults.svelte" import Tr from "../Base/Tr.svelte" import Translations from "../i18n/Translations" + import type { FilterSearchResult } from "../../Logic/Search/FilterSearch" export let state: ThemeViewState - let activeFilters: Store = state.layerState.activeFilters.map(fs => fs.filter(f => + let activeFilters: Store<(ActiveFilter & FilterSearchResult)[]> = state.layerState.activeFilters.map(fs => fs.filter(f => (f.filter.options[0].fields.length === 0) && - Constants.priviliged_layers.indexOf(f.layer.id) < 0)) + Constants.priviliged_layers.indexOf(f.layer.id) < 0) + .map(af => { + const index = af.control.data + const r : FilterSearchResult & ActiveFilter = { ...af, index, option: af.filter.options[index] } + return r + })) let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview let searchTerm = state.searchState.searchTerm @@ -23,13 +29,15 @@ {#if $searchTerm.length === 0 && $activeFilters.length === 0 }
- + + +
{/if} - + - + {#if $allowOtherThemes}