Review of many feature switches and iframe context; fix OH-vis when ranges are not given

This commit is contained in:
Pieter Vander Vennet 2024-06-23 02:54:53 +02:00
parent ecfa7d3d1d
commit afea9adacb
21 changed files with 157 additions and 88 deletions

View file

@ -292,6 +292,7 @@
}
}
},
"metacondition": "_loggedIn=true",
"mappings": [
{
"if": "id~.*/-.*",

View file

@ -17,8 +17,7 @@
"description": "This block shows the known images which are linked with the `image`-keys, but also via `mapillary` and `wikidata` and shows the button to upload new images",
"render": {
"*": "{image_carousel()}{image_upload()}"
},
"classes": "my-4"
}
},
{
"id": "mapillary",

View file

@ -325,7 +325,7 @@
"opening_hours": {
"all_days_from": "Opened every day {ranges}",
"closed_permanently": "Closed for an unknown duration",
"closed_until": "Closed until {date}",
"closed_until": "Opens at {date}",
"error": "Could not parse the opening hours",
"error_loading": "Error: could not visualize these opening hours.",
"friday": "On friday {ranges}",
@ -359,6 +359,7 @@
"versionInfo": "v{version} - generated on {date}"
},
"pickLanguage": "Select language",
"poweredByMapComplete": "Powered by MapComplete - crowdsourced, thematic maps with OpenStreetMap",
"poweredByOsm": "Powered by OpenStreetMap",
"questionBox": {
"answeredMultiple": "You answered {answered} questions",
@ -394,6 +395,7 @@
"searching": "Searching…"
},
"searchAnswer": "Search an option…",
"seeIndex": "See the overview with all thematic maps",
"share": "Share",
"sharescreen": {
"copiedToClipboard": "Link copied to clipboard",

View file

@ -10754,6 +10754,26 @@
},
"question": "What accessibility features should be applied?"
},
"add-new-feature": {
"mappings": {
"0": {
"then": "Adding a new feature is done with the button at the bottom left. Clicking the map does nothing"
},
"1": {
"then": "When clicking or tapping the map, a marker pops up where a new feature is added"
},
"2": {
"then": "When right-clicking or long-pressing the map, a marker pops up where a new feature can be added"
},
"3": {
"then": "When clicking or tapping the map, a marker pops up where a new feature can be added. Additionally, a button at the bottom left is shown"
},
"4": {
"then": "When right-clicking or long-pressing the map, a marker pops up where a new feature can be added. Additionally, a button at the bottom left is shown"
}
},
"question": "How should the menu to add a new feature be opened?"
},
"all-questions-at-once": {
"mappings": {
"0": {

View file

@ -6,22 +6,26 @@ import { UIEventSource } from "../UIEventSource"
import { QueryParameters } from "../Web/QueryParameters"
import Constants from "../../Models/Constants"
import { Utils } from "../../Utils"
import { Query } from "pg"
class FeatureSwitchUtils {
/** Helper function to initialize feature switches
*
*/
static initSwitch(key: string, deflt: boolean, documentation: string): UIEventSource<boolean> {
const defaultValue = deflt
const queryParam = QueryParameters.GetQueryParameter(
key,
"" + defaultValue,
documentation,
{ stackOffset: -1 }
{ stackOffset: -1 },
)
// It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened
return queryParam.sync(
(str) => (str === undefined ? defaultValue : str !== "false"),
[],
(b) => (b == defaultValue ? undefined : "" + b)
(b) => (b == defaultValue ? undefined : "" + b),
)
}
}
@ -33,7 +37,7 @@ export class OsmConnectionFeatureSwitches {
this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter(
"fake-user",
false,
"If true, 'dryrun' mode is activated and a fake user account is loaded"
"If true, 'dryrun' mode is activated and a fake user account is loaded",
)
}
}
@ -69,19 +73,41 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
super()
this.layoutToUse = layoutToUse
// Helper function to initialize feature switches
const legacyRewrite: Record<string, string | string[]> = {
"fs-userbadge": "fs-enable-login",
"fs-layers": ["fs-filter", "fs-background"],
}
for (const key in legacyRewrite) {
let intoList = legacyRewrite[key]
if (!QueryParameters.wasInitialized(key)) {
continue
}
if (typeof intoList === "string") {
intoList = [intoList]
}
for (const into of intoList) {
if (!QueryParameters.wasInitialized(into)) {
const v = QueryParameters.GetQueryParameter(key, "", "").data
console.log("Adding url param due to legacy:", key, "-->", into, "(", v + ")")
QueryParameters.GetQueryParameter(into, "", "").setData(v)
}
}
}
this.featureSwitchEnableLogin = FeatureSwitchUtils.initSwitch(
"fs-enable-login",
layoutToUse?.enableUserBadge ?? true,
"Disables/Enables logging in and thus disables editing all together. This effectively puts MapComplete into read-only mode."
"Disables/Enables logging in and thus disables editing all together. This effectively puts MapComplete into read-only mode.",
)
{
if (QueryParameters.wasInitialized("fs-userbadge")) {
// userbadge is the legacy name for 'enable-login'
this.featureSwitchEnableLogin.setData(
QueryParameters.GetBooleanQueryParameter("fs-userbadge", undefined, "Legacy")
.data
.data,
)
}
}
@ -89,60 +115,60 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
this.featureSwitchSearch = FeatureSwitchUtils.initSwitch(
"fs-search",
layoutToUse?.enableSearch ?? true,
"Disables/Enables the search bar"
"Disables/Enables the search bar",
)
this.featureSwitchBackgroundSelection = FeatureSwitchUtils.initSwitch(
"fs-background",
layoutToUse?.enableBackgroundLayerSelection ?? true,
"Disables/Enables the background layer control"
"Disables/Enables the background layer control where a user can enable e.g. aerial imagery",
)
this.featureSwitchFilter = FeatureSwitchUtils.initSwitch(
"fs-filter",
layoutToUse?.enableLayers ?? true,
"Disables/Enables the filter view"
"Disables/Enables the filter view where a user can enable/disable MapComplete-layers or filter for certain properties",
)
this.featureSwitchWelcomeMessage = FeatureSwitchUtils.initSwitch(
"fs-welcome-message",
true,
"Disables/enables the help menu or welcome message"
"Disables/enables the help menu or welcome message",
)
this.featureSwitchCommunityIndex = FeatureSwitchUtils.initSwitch(
"fs-community-index",
this.featureSwitchEnableLogin.data,
"Disables/enables the button to get in touch with the community"
"Disables/enables the button to get in touch with the community",
)
this.featureSwitchExtraLinkEnabled = FeatureSwitchUtils.initSwitch(
"fs-iframe-popout",
true,
"Disables/Enables the extraLink button. By default, if in iframe mode and the welcome message is hidden, a popout button to the full mapcomplete instance is shown instead (unless disabled with this switch or another extraLink button is enabled)"
"Disables/Enables the extraLink button. By default, if in iframe mode and the welcome message is hidden, a popout button to the full mapcomplete instance is shown instead (unless disabled with this switch or another extraLink button is enabled)",
)
this.featureSwitchBackToThemeOverview = FeatureSwitchUtils.initSwitch(
"fs-homepage-link",
layoutToUse?.enableMoreQuests ?? true,
"Disables/Enables the various links which go back to the index page with the theme overview"
"Disables/Enables the various links which go back to the index page with the theme overview",
)
this.featureSwitchShareScreen = FeatureSwitchUtils.initSwitch(
"fs-share-screen",
layoutToUse?.enableShareScreen ?? true,
"Disables/Enables the 'Share-screen'-tab in the welcome message"
"Disables/Enables the 'Share-screen'-tab in the welcome message",
)
this.featureSwitchGeolocation = FeatureSwitchUtils.initSwitch(
"fs-geolocation",
layoutToUse?.enableGeolocation ?? true,
"Disables/Enables the geolocation button"
"Disables/Enables the geolocation button",
)
this.featureSwitchShowAllQuestions = FeatureSwitchUtils.initSwitch(
"fs-all-questions",
layoutToUse?.enableShowAllQuestions ?? false,
"Always show all questions"
"Always show all questions",
)
this.featureSwitchEnableExport = FeatureSwitchUtils.initSwitch(
"fs-export",
layoutToUse?.enableExportButton ?? true,
"Enable the export as GeoJSON and CSV button"
"Enable the export as GeoJSON and CSV button",
)
let testingDefaultValue = false
@ -156,59 +182,59 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
this.featureSwitchIsTesting = QueryParameters.GetBooleanQueryParameter(
"test",
testingDefaultValue,
"If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org"
"If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org",
)
this.featureSwitchIsDebugging = QueryParameters.GetBooleanQueryParameter(
"debug",
false,
"If true, shows some extra debugging help such as all the available tags on every object"
"If true, shows some extra debugging help such as all the available tags on every object",
)
this.featureSwitchMorePrivacy = QueryParameters.GetBooleanQueryParameter(
"moreprivacy",
layoutToUse.enableMorePrivacy,
"If true, the location distance indication will not be written to the changeset and other privacy enhancing measures might be taken."
"If true, the location distance indication will not be written to the changeset and other privacy enhancing measures might be taken.",
)
this.overpassUrl = QueryParameters.GetQueryParameter(
"overpassUrl",
(layoutToUse?.overpassUrl ?? Constants.defaultOverpassUrls).join(","),
"Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter"
"Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter",
).sync(
(param) => param?.split(","),
[],
(urls) => urls?.join(",")
(urls) => urls?.join(","),
)
this.overpassTimeout = UIEventSource.asInt(
QueryParameters.GetQueryParameter(
"overpassTimeout",
"" + layoutToUse?.overpassTimeout,
"Set a different timeout (in seconds) for queries in overpass"
)
"Set a different timeout (in seconds) for queries in overpass",
),
)
this.overpassMaxZoom = UIEventSource.asFloat(
QueryParameters.GetQueryParameter(
"overpassMaxZoom",
"" + layoutToUse?.overpassMaxZoom,
" point to switch between OSM-api and overpass"
)
" point to switch between OSM-api and overpass",
),
)
this.osmApiTileSize = UIEventSource.asInt(
QueryParameters.GetQueryParameter(
"osmApiTileSize",
"" + layoutToUse?.osmApiTileSize,
"Tilesize when the OSM-API is used to fetch data within a BBOX"
)
"Tilesize when the OSM-API is used to fetch data within a BBOX",
),
)
this.backgroundLayerId = QueryParameters.GetQueryParameter(
"background",
layoutToUse?.defaultBackgroundId,
"The id of the background layer to start with"
"The id of the background layer to start with",
)
}
}

View file

@ -297,6 +297,7 @@ export default class UserRelatedState {
_applicationOpened: new Date().toISOString(),
_supports_sharing:
typeof window === "undefined" ? "no" : window.navigator.share ? "yes" : "no",
_iframe: Utils.isIframe ? "yes" : "no"
})
for (const key in Constants.userJourney) {

View file

@ -40,8 +40,8 @@ export default class CopyrightPanel extends Combine {
const t = Translations.t.general.attribution
const layoutToUse = state.layout
const iconAttributions: BaseUIElement[] = layoutToUse
.getUsedImages()
const iconAttributions: BaseUIElement[] = Utils.Dedup(layoutToUse
.getUsedImages())
.map(CopyrightPanel.IconAttribution)
let maintainer: BaseUIElement = undefined

View file

@ -11,10 +11,9 @@
export let state: SpecialVisualizationState
let theme = state.layout?.id ?? ""
let config: ExtraLinkConfig = state.layout.extraLink
const isIframe = window !== window.top
let basepath = window.location.host
let showWelcomeMessageSwitch = state.featureSwitches.featureSwitchWelcomeMessage
const isIframe = Utils.isIframe
const t = Translations.t.general
const href = state.mapProperties.location.map(
(loc) => {
@ -36,7 +35,7 @@
href={$href}
target={config.newTab ? "_blank" : ""}
rel="noopener"
class="pointer-events-auto flex rounded-full border-black"
class="button pointer-events-auto flex rounded-full border-black"
>
<Icon icon={config.icon} clss="w-6 h-6 m-2" />

View file

@ -15,6 +15,7 @@
import Location_refused from "../../assets/svg/Location_refused.svelte"
import Location from "../../assets/svg/Location.svelte"
import ChevronDoubleLeft from "@babeard/svelte-heroicons/mini/ChevronDoubleLeft"
import Constants from "../../Models/Constants"
/**
* The theme introduction panel
@ -149,13 +150,20 @@
{/if}
</div>
{#if Utils.isIframe}
<div class="flex justify-end link-underline">
<a href="https://mapcomplete.org" target="_blank">
<Tr t={Translations.t.general.poweredByMapComplete}/>
</a>
</div>
{:else}
<If condition={state.featureSwitches.featureSwitchBackToThemeOverview}>
<div class="link-underline m-2 mx-4 flex w-full">
<!-- bottom buttons, a bit hidden away: switch layout -->
<a class="flex w-fit items-center justify-end" href={Utils.HomepageLink()}>
<ChevronDoubleLeft class="h-4 w-4" />
<Tr t={Translations.t.general.backToIndex} />
</a>
</div>
</If>
{/if}
</div>

View file

@ -42,9 +42,10 @@
let knownImages = comparisonState.bindD((ct) => ct.knownImages)
let propertyKeysExternal = comparisonState.mapD((ct) => ct.propertyKeysExternal)
let hasDifferencesAtStart = comparisonState.mapD((ct) => ct.hasDifferencesAtStart)
let enableLogin= state.featureSwitches.featureSwitchEnableLogin
</script>
{#if !$sourceUrl}
{#if !$sourceUrl || !$enableLogin}
<!-- empty block -->
{:else if $externalData === undefined}
<Loading />

View file

@ -54,10 +54,9 @@ export class ImageCarousel extends Toggle {
)
super(
new SlideShow(uiElements).SetClass("w-full"),
new SlideShow(uiElements).SetClass("w-full block w-full my-4"),
undefined,
uiElements.map((els) => els.length > 0)
)
this.SetClass("block w-full")
}
}

View file

@ -25,11 +25,14 @@
const t = Translations.t.image.nearby
let expanded = false
let enableLogin = state.featureSwitches.featureSwitchEnableLogin
</script>
{#if enableLogin.data}
<AccordionSingle>
<span slot="header" class="p-2 text-base">
<Tr t={t.seeNearby} />
</span>
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable} {layer} />
</AccordionSingle>
{/if}

View file

@ -58,7 +58,7 @@
<LoginButton clss="small w-full" osmConnection={state.osmConnection} slot="not-logged-in">
<Tr t={Translations.t.image.pleaseLogin} />
</LoginButton>
<div class="flex flex-col">
<div class="flex flex-col my-4">
<UploadingImageCounter {state} {tags} />
{#each $errors as error}
<Tr t={error} cls="alert" />

View file

@ -932,9 +932,11 @@ export class ToTextualDescription {
public static createTextualDescriptionFor(
oh: opening_hours,
ranges: OpeningRange[][]
): Translation {
): Translation | undefined {
const t = Translations.t.general.opening_hours
if(!ranges){
return undefined
}
if (!ranges?.some((r) => r.length > 0)) {
// <!-- No changes to the opening hours in the next week; probably open 24/7, permanently closed, opening far in the future or unkown -->
if (oh.getNextChange() === undefined) {
@ -1029,9 +1031,9 @@ export class ToTextualDescription {
})
}
private static createRangesFor(ranges: OpeningRange[]): Translation {
private static createRangesFor(ranges: OpeningRange[]): Translation | undefined {
if (ranges.length === 0) {
// return undefined
return undefined
}
let tr = ToTextualDescription.createRangeFor(ranges[0])
for (let i = 1; i < ranges.length; i++) {

View file

@ -26,7 +26,6 @@ export default class OpeningHoursVisualization extends Toggle {
constructor(
tags: UIEventSource<Record<string, string>>,
state: { osmConnection?: OsmConnection },
key: string,
prefix = "",
postfix = ""
@ -56,7 +55,7 @@ export default class OpeningHoursVisualization extends Toggle {
)
Locale.language.mapD((lng) => {
console.debug("Setting OH description to", lng, textual)
vis.ConstructElement().ariaLabel = textual.textFor(lng)
vis.ConstructElement().ariaLabel = textual?.textFor(lng)
})
return vis
})
@ -75,18 +74,14 @@ export default class OpeningHoursVisualization extends Toggle {
ranges: OpeningRange[][],
lastMonday: Date
): BaseUIElement {
/* First, a small sanity check. The business might be permanently closed, 24/7 opened or something other special
* So, we have to handle the case that ranges is completely empty*/
if (ranges.filter((range) => range.length > 0).length === 0) {
return OpeningHoursVisualization.ShowSpecialCase(oh).SetClass(
"p-4 rounded-full block bg-gray-200"
)
}
/** With all the edge cases handled, we can actually construct the table! **/
// First, a small sanity check. The business might be permanently closed, 24/7 opened or something other special
if (ranges.some((range) => range.length > 0)) {
// The normal case: we have items for the coming days
return OpeningHoursVisualization.ConstructVizTable(oh, ranges, lastMonday)
}
// The special case that range is completely empty
return OpeningHoursVisualization.ShowSpecialCase(oh)
}
private static ConstructVizTable(
oh: any,
@ -308,6 +303,6 @@ export default class OpeningHoursVisualization extends Toggle {
opensAtDate.getHours(),
opensAtDate.getMinutes()
)}`
return Translations.t.general.opening_hours.closed_until.Subs({ date: willOpenAt })
return Translations.t.general.opening_hours.closed_until.Subs({ date: opensAtDate.toLocaleString() })
}
}

View file

@ -94,6 +94,8 @@
let answered: number = 0
let skipped: number = 0
let loginEnabled = state.featureSwitches.featureSwitchEnableLogin
function skip(question: { id: string }, didAnswer: boolean = false) {
skippedQuestions.data.add(question.id) // Must use ID, the config object might be a copy of the original
skippedQuestions.ping()
@ -108,6 +110,7 @@
}
</script>
{#if $loginEnabled}
<div
bind:this={questionboxElem}
aria-live="polite"
@ -197,3 +200,4 @@
</div>
{/if}
</div>
{/if}

View file

@ -83,7 +83,7 @@
{config.id}
</span>
{/if}
{#if config.question && (!editingEnabled || $editingEnabled)}
{#if config.question}
{#if editMode}
<TagRenderingQuestion
{config}
@ -106,7 +106,8 @@
</TagRenderingQuestion>
{:else}
<div class="low-interaction flex items-center justify-between overflow-hidden rounded pl-2">
<TagRenderingAnswer id={answerId} {config} {tags} {selectedElement} {state} {layer} />
<TagRenderingAnswer id={answerId} {config} {tags} {selectedElement} {state} {layer} extraClasses="my-2"/>
{#if (!editingEnabled || $editingEnabled)}
<EditButton
arialabel={config.editButtonAriaLabel}
ariaLabelledBy={answerId}
@ -114,11 +115,10 @@
editMode = true
}}
/>
{/if}
</div>
{/if}
{:else}
<div class="h-full w-full overflow-auto">
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
</div>
{/if}
</div>

View file

@ -794,7 +794,7 @@ export default class SpecialVisualizations {
"A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`",
constr: (state, tagSource: UIEventSource<any>, args) => {
const [key, prefix, postfix] = args
return new OpeningHoursVisualization(tagSource, state, key, prefix, postfix)
return new OpeningHoursVisualization(tagSource, key, prefix, postfix)
},
},
{

View file

@ -1,4 +1,7 @@
<script lang="ts">
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
</script>
<main />
<main >
<ToSvelte construct={new OpeningHoursVisualization()}
</main>

View file

@ -124,11 +124,11 @@
state.mapProperties.installCustomKeyboardHandler(viewport)
let canZoomIn = mapproperties.maxzoom.map(
(mz) => mapproperties.zoom.data < mz,
[mapproperties.zoom]
[mapproperties.zoom],
)
let canZoomOut = mapproperties.minzoom.map(
(mz) => mapproperties.zoom.data > mz,
[mapproperties.zoom]
[mapproperties.zoom],
)
function updateViewport() {
@ -165,7 +165,7 @@
onDestroy(
rasterLayer.addCallbackAndRunD((l) => {
rasterLayerName = l.properties.name
})
}),
)
let previewedImage = state.previewedImage
@ -196,7 +196,7 @@
let openMapButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
let openMenuButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
let openCurrentViewLayerButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(
undefined
undefined,
)
let _openNewElementButton: HTMLButtonElement
let openNewElementButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
@ -478,7 +478,7 @@
<FloatOver on:close={() => state.guistate.themeIsOpened.setData(false)}>
<span slot="close-button"><!-- Disable the close button --></span>
<TabbedGroup
condition1={state.featureSwitches.featureSwitchFilter}
condition1={state.featureSwitches.featureSwitchEnableExport}
tab={state.guistate.themeViewTabIndex}
>
<div slot="post-tablist">
@ -572,9 +572,14 @@
<div class="link-underline links-w-full m-2 flex flex-col gap-y-1" slot="content0">
<Tr t={Translations.t.general.aboutMapComplete.intro} />
<a class="flex" href={Utils.HomepageLink()}>
<Add class="h-6 w-6" />
{#if Utils.isIframe}
<Tr t={Translations.t.general.seeIndex} />
{:else}
<Tr t={Translations.t.general.backToIndex} />
{/if}
</a>
<a class="flex" href="https://github.com/pietervdvn/MapComplete/" target="_blank">

View file

@ -1,4 +1,3 @@
import colors from "./assets/colors.json"
import DOMPurify from "dompurify"
export class Utils {
@ -8,7 +7,6 @@ export class Utils {
* This is a workaround and yet another hack
*/
public static runningFromConsole = typeof window === "undefined"
public static readonly assets_path = "./assets/svg/"
public static externalDownloadFunction: (
url: string,
headers?: any
@ -146,6 +144,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
>()
public static readonly isIframe = !Utils.runningFromConsole && window !== window.top
public static initDomPurify() {
if (Utils.runningFromConsole) {
return