forked from MapComplete/MapComplete
		
	Merge develop
This commit is contained in:
		
						commit
						fec3608ca4
					
				
					 57 changed files with 1160 additions and 594 deletions
				
			
		|  | @ -8,6 +8,8 @@ | |||
|   import { Utils } from "../../Utils" | ||||
|   import type { ValidatorType } from "../InputElement/Validators" | ||||
|   import InputHelper from "../InputElement/InputHelper.svelte" | ||||
|   import { Translation } from "../i18n/Translation" | ||||
|   import Tr from "../Base/Tr.svelte" | ||||
| 
 | ||||
|   export let filteredLayer: FilteredLayer | ||||
|   export let option: FilterConfigOption | ||||
|  | @ -36,7 +38,7 @@ | |||
|     appliedFilter?.setData(FilteredLayer.fieldsToString(properties)) | ||||
|   } | ||||
| 
 | ||||
|   let firstValue : UIEventSource<string> | ||||
|   let firstValue: UIEventSource<string> | ||||
|   for (const field of option.fields) { | ||||
|     // A bit of cheating: the 'parts' will have '}' suffixed for fields | ||||
|     const src = new UIEventSource<string>(initialState[field.name] ?? "") | ||||
|  | @ -47,9 +49,10 @@ | |||
|     onDestroy( | ||||
|       src.stabilized(200).addCallback(() => { | ||||
|         setFields() | ||||
|       }), | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
|   let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined) | ||||
| </script> | ||||
| 
 | ||||
| <div class="low-interaction p-1 rounded-2xl px-3" class:interactive={$firstValue?.length > 0}> | ||||
|  | @ -58,11 +61,15 @@ | |||
|       <!-- This is a field! --> | ||||
|       <span class="mx-1"> | ||||
|         <InputHelper value={fieldValues[part["subs"]]} type={fieldTypes[part["subs"]]}> | ||||
|           <ValidatedInput slot="fallback" value={fieldValues[part["subs"]]} type={fieldTypes[part["subs"]]} /> | ||||
|           <ValidatedInput slot="fallback" value={fieldValues[part["subs"]]} type={fieldTypes[part["subs"]]} | ||||
|                           {feedback} /> | ||||
|         </InputHelper> | ||||
|       </span> | ||||
|     {:else} | ||||
|       {@html part["message"]} | ||||
|     {/if} | ||||
|   {/each} | ||||
|   {#if $feedback} | ||||
|     <Tr cls="alert" t={$feedback}/> | ||||
|   {/if} | ||||
| </div> | ||||
|  |  | |||
|  | @ -90,13 +90,13 @@ | |||
|       <LoginButton osmConnection={state.osmConnection} slot="not-logged-in" /> | ||||
|       <div class="flex items-center gap-x-4"> | ||||
|         {#if $userdetails.img} | ||||
|           <img src={$userdetails.img} class="h-14 w-14 rounded-full" /> | ||||
|           <img alt="avatar" src={$userdetails.img} class="h-14 w-14 rounded-full" /> | ||||
|         {/if} | ||||
|         <b>{$userdetails.name}</b> | ||||
|       </div> | ||||
|     </LoginToggle> | ||||
| 
 | ||||
|     <Page {onlyLink} shown={pg.usersettings} bodyPadding="p-0"> | ||||
|     <Page {onlyLink} shown={pg.usersettings} bodyPadding="p-0 pb-4"> | ||||
|       <svelte:fragment slot="header"> | ||||
|         <CogIcon /> | ||||
|         <Tr t={UserRelatedState.usersettingsConfig.title.GetRenderValue({})} /> | ||||
|  |  | |||
|  | @ -220,8 +220,8 @@ | |||
|         <td/> | ||||
|         {/if} | ||||
|       {#each range(7) as wd} | ||||
|         <OHCell type="half" {h} {wd} on:start={() => startSelection(wd, h)} on:end={() => endSelection(wd, h)} | ||||
|                 on:move={() => moved(wd, h)} on:clear={() => clearSelection()} /> | ||||
|         <OHCell type="half" {h} {wd} on:start={() => startSelection(wd, h + 0.5)} on:end={() => endSelection(wd, h + 0.5)} | ||||
|                 on:move={() => moved(wd, h + 0.5)} on:clear={() => clearSelection()} /> | ||||
|       {/each} | ||||
|     </tr> | ||||
| 
 | ||||
|  |  | |||
|  | @ -15,9 +15,6 @@ import UrlValidator from "./Validators/UrlValidator" | |||
| import PhoneValidator from "./Validators/PhoneValidator" | ||||
| import OpeningHoursValidator from "./Validators/OpeningHoursValidator" | ||||
| import ColorValidator from "./Validators/ColorValidator" | ||||
| import BaseUIElement from "../BaseUIElement" | ||||
| import Combine from "../Base/Combine" | ||||
| import Title from "../Base/Title" | ||||
| import SimpleTagValidator from "./Validators/SimpleTagValidator" | ||||
| import ImageUrlValidator from "./Validators/ImageUrlValidator" | ||||
| import TagKeyValidator from "./Validators/TagKeyValidator" | ||||
|  | @ -30,6 +27,7 @@ import SlopeValidator from "./Validators/SlopeValidator" | |||
| import VeloparkValidator from "./Validators/VeloparkValidator" | ||||
| import NameSuggestionIndexValidator from "./Validators/NameSuggestionIndexValidator" | ||||
| import CurrencyValidator from "./Validators/CurrencyValidator" | ||||
| import RegexValidator from "./Validators/RegexValidator" | ||||
| 
 | ||||
| export type ValidatorType = (typeof Validators.availableTypes)[number] | ||||
| 
 | ||||
|  | @ -64,6 +62,7 @@ export default class Validators { | |||
|         "velopark", | ||||
|         "nsi", | ||||
|         "currency", | ||||
|         "regex" | ||||
|     ] as const | ||||
| 
 | ||||
|     public static readonly AllValidators: ReadonlyArray<Validator> = [ | ||||
|  | @ -95,6 +94,7 @@ export default class Validators { | |||
|         new VeloparkValidator(), | ||||
|         new NameSuggestionIndexValidator(), | ||||
|         new CurrencyValidator(), | ||||
|         new RegexValidator() | ||||
|     ] | ||||
| 
 | ||||
|     private static _byType = Validators._byTypeConstructor() | ||||
|  |  | |||
							
								
								
									
										22
									
								
								src/UI/InputElement/Validators/RegexValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/UI/InputElement/Validators/RegexValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | |||
| import StringValidator from "./StringValidator" | ||||
| import { s } from "vitest/dist/env-afee91f0" | ||||
| import { Translation } from "../../i18n/Translation" | ||||
| import Translations from "../../i18n/Translations" | ||||
| 
 | ||||
| export default class RegexValidator extends StringValidator{ | ||||
|     constructor() { | ||||
|         super("regex", "Validates a regex") | ||||
|     } | ||||
| 
 | ||||
|     getFeedback(s: string): Translation | undefined { | ||||
|         try{ | ||||
|             new RegExp(s) | ||||
|         }catch (e) { | ||||
|             return Translations.T("Not a valid Regex: "+e) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     isValid(s: string): boolean { | ||||
|         return this.getFeedback(s) === undefined | ||||
|     } | ||||
| } | ||||
|  | @ -9,8 +9,8 @@ | |||
|   import BackButton from "../Base/BackButton.svelte" | ||||
|   import NextButton from "../Base/NextButton.svelte" | ||||
|   import WikipediaPanel from "../Wikipedia/WikipediaPanel.svelte" | ||||
|   import { createEventDispatcher } from "svelte" | ||||
|   import Plantnet_logo from "../../assets/svg/Plantnet_logo.svelte" | ||||
|   import ArrowPath from "@babeard/svelte-heroicons/mini/ArrowPath" | ||||
| 
 | ||||
|   /** | ||||
|    * The main entry point for the plantnet wizard | ||||
|  | @ -23,7 +23,6 @@ | |||
|    */ | ||||
|   export let imageUrls: Store<string[]> | ||||
|   export let onConfirm: (wikidataId: string) => void | ||||
|   const dispatch = createEventDispatcher<{ selected: string }>() | ||||
|   let collapsedMode = true | ||||
|   let options: UIEventSource<PlantNetSpeciesMatch[]> = new UIEventSource<PlantNetSpeciesMatch[]>( | ||||
|     undefined | ||||
|  | @ -38,18 +37,20 @@ | |||
| 
 | ||||
|   let done = false | ||||
| 
 | ||||
|   function speciesSelected(species: PlantNetSpeciesMatch) { | ||||
|   function speciesSelected(species: string) { | ||||
|     console.log("Selected:", species) | ||||
|     selectedOption = species | ||||
|   } | ||||
| 
 | ||||
|   async function detectSpecies() { | ||||
|     error = undefined | ||||
|     collapsedMode = false | ||||
| 
 | ||||
|     try { | ||||
|       const result = await PlantNet.query(imageUrls.data.slice(0, 5)) | ||||
|       options.set(result.results.filter((r) => r.score > 0.005).slice(0, 8)) | ||||
|     } catch (e) { | ||||
|       console.error("Caught", e) | ||||
|       error = e | ||||
|     } | ||||
|   } | ||||
|  | @ -60,8 +61,12 @@ | |||
|     <button class="w-full" on:click={detectSpecies}> | ||||
|       <Tr t={t.button} /> | ||||
|     </button> | ||||
|   {:else if $error !== undefined} | ||||
|   {:else if error !== undefined} | ||||
|     <Tr cls="alert" t={t.error.Subs({ error })} /> | ||||
|     <button on:click={() => detectSpecies()}> | ||||
|       <ArrowPath class="w-6 h-6"/> | ||||
|       <Tr t={Translations.t.general.retry}/> | ||||
|     </button> | ||||
|   {:else if $imageUrls.length === 0} | ||||
|     <!-- No urls are available, show the explanation instead--> | ||||
|     <div class=" border-region relative mb-1 p-2"> | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ | |||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
|   import Searchbar from "../Base/Searchbar.svelte" | ||||
|   import Translations from "../i18n/Translations" | ||||
|   import { Utils } from "../../Utils" | ||||
| 
 | ||||
|   export let tags: UIEventSource<Record<string, any>> | ||||
|   export let tagKeys = tags.map((tgs) => (tgs === undefined ? [] : Object.keys(tgs))) | ||||
|  | @ -34,10 +35,19 @@ | |||
|   const metaKeys: string[] = [].concat(...SimpleMetaTaggers.metatags.map((k) => k.keys)) | ||||
|   let allCalculatedTags = new Set<string>([...calculatedTags, ...metaKeys]) | ||||
|   let search = new UIEventSource<string>("") | ||||
| 
 | ||||
|   function downloadAsJson(){ | ||||
|     Utils.offerContentsAsDownloadableFile( | ||||
|       JSON.stringify(tags.data, null, "  "), "tags-"+(tags.data.id ?? layer?.id ?? "")+".json" | ||||
|     ) | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| <section> | ||||
|   <Searchbar value={search} placeholder={Translations.T("Search a key")}></Searchbar> | ||||
|   <button class="as-link" on:click={() => downloadAsJson()}> | ||||
|     Download as JSON | ||||
|   </button> | ||||
|   <table class="zebra-table break-all"> | ||||
|     <tr> | ||||
|       <th>Key</th> | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ | |||
|     .getSchemaStartingWith(schema.path) | ||||
|     .filter((part) => part.path.length - 1 === schema.path.length) | ||||
| 
 | ||||
|   let usesOverride = value["builtin"] !== undefined | ||||
|   let usesOverride = value?.["builtin"] !== undefined | ||||
| 
 | ||||
|   function schemaForMultitype() { | ||||
|     const sch = { ...schema } | ||||
|  | @ -138,7 +138,7 @@ | |||
|       </div> | ||||
|     {:else if typeof value === "string"} | ||||
|       Builtin: <b>{value}</b> | ||||
|     {:else if value["builtin"]} | ||||
|     {:else if value?.["builtin"]} | ||||
|       reused tagrendering <span class="font-bold">{JSON.stringify(value["builtin"])}</span> | ||||
|     {:else} | ||||
|       <Tr cls="font-bold" t={Translations.T(value?.question ?? value?.render)} /> | ||||
|  |  | |||
|  | @ -159,6 +159,9 @@ export abstract class EditJsonState<T> { | |||
|     } | ||||
| 
 | ||||
|     public getSchemaStartingWith(path: string[]) { | ||||
|         if(path === undefined){ | ||||
|             return undefined | ||||
|         } | ||||
|         return this.schema.filter( | ||||
|             (sch) => | ||||
|                 !path.some((part, i) => !(sch.path.length > path.length && sch.path[i] === part)) | ||||
|  |  | |||
|  | @ -5,18 +5,23 @@ | |||
|   import { TrashIcon } from "@babeard/svelte-heroicons/mini" | ||||
|   import ShowConversionMessage from "./ShowConversionMessage.svelte" | ||||
|   import Markdown from "../Base/Markdown.svelte" | ||||
|   import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" | ||||
|   import type { | ||||
|     QuestionableTagRenderingConfigJson | ||||
|   } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" | ||||
|   import CollapsedTagRenderingPreview from "./CollapsedTagRenderingPreview.svelte" | ||||
|   import { Accordion } from "flowbite-svelte" | ||||
|   import { Utils } from "../../Utils" | ||||
| 
 | ||||
|   export let state: EditJsonState<any> | ||||
|   export let path: (string | number)[] = [] | ||||
|   let schema: ConfigMeta = state.getSchema(path)[0] | ||||
|   console.log("SBA got schema", schema, "for path", path) | ||||
| 
 | ||||
|   let title = schema.path.at(-1) | ||||
| 
 | ||||
|   let title = schema?.path?.at(-1) | ||||
|   let singular = title | ||||
|   if (title?.endsWith("s")) { | ||||
|     singular = title.slice(0, title.length - 1) | ||||
|     singular = title?.slice(0, title.length - 1) | ||||
|   } | ||||
|   let article = "a" | ||||
|   if (singular?.match(/^[aeoui]/)) { | ||||
|  | @ -25,18 +30,20 @@ | |||
| 
 | ||||
|   const isTagRenderingBlock = path.length === 1 && path[0] === "tagRenderings" | ||||
| 
 | ||||
|   if (isTagRenderingBlock) { | ||||
|   if (isTagRenderingBlock && schema !== undefined) { | ||||
|     schema = { ...schema } | ||||
|     schema.description = undefined | ||||
|   } | ||||
| 
 | ||||
|   const subparts: ConfigMeta[] = state | ||||
|     .getSchemaStartingWith(schema.path) | ||||
|     .filter((part) => part.path.length - 1 === schema.path.length) | ||||
|     .getSchemaStartingWith(schema?.path) | ||||
|     ?.filter((part) => part.path.length - 1 === schema?.path?.length) | ||||
|   let messages = state.messagesFor(path) | ||||
| 
 | ||||
|   let datapath = path | ||||
|   const currentValue = state.getStoreFor<(string | QuestionableTagRenderingConfigJson)[]>(datapath) | ||||
|   currentValue.set(Utils.DedupT(currentValue.data)) | ||||
|   console.log("Current value is", currentValue.data) | ||||
|   if (currentValue.data === undefined) { | ||||
|     currentValue.setData([]) | ||||
|   } | ||||
|  | @ -62,68 +69,69 @@ | |||
|     currentValue.ping() | ||||
|   } | ||||
| </script> | ||||
| {#if schema !== undefined} | ||||
|   <div class="pl-2"> | ||||
|     <h3>{schema.path.at(-1)}</h3> | ||||
| 
 | ||||
| <div class="pl-2"> | ||||
|   <h3>{schema.path.at(-1)}</h3> | ||||
| 
 | ||||
|   {#if subparts.length > 0} | ||||
|     <Markdown src={schema.description} /> | ||||
|   {/if} | ||||
|   {#if $currentValue === undefined} | ||||
|     No array defined | ||||
|   {:else if !Array.isArray($currentValue)} | ||||
|     Not an array: {typeof $currentValue} | ||||
|     {JSON.stringify(path)} | ||||
|     {JSON.stringify($currentValue).slice(0, 120)} | ||||
|   {:else if $currentValue?.length === 0} | ||||
|     No values are defined | ||||
|     {#if $messages.length > 0} | ||||
|       {#each $messages as message} | ||||
|         <ShowConversionMessage {message} /> | ||||
|       {/each} | ||||
|     {#if subparts.length > 0} | ||||
|       <Markdown src={schema.description} /> | ||||
|     {/if} | ||||
|   {:else if subparts.length === 0} | ||||
|     <!-- We need an array of values, so we use the typehint of the _parent_ element as field --> | ||||
|     {#each $currentValue as value, i} | ||||
|       <div class="flex w-full"> | ||||
|         <SchemaBasedField {state} {schema} path={fusePath(i)} /> | ||||
|         <button | ||||
|           class="h-fit w-fit rounded-full border border-black p-1" | ||||
|           on:click={() => { | ||||
|     {#if $currentValue === undefined} | ||||
|       No array defined | ||||
|     {:else if !Array.isArray($currentValue)} | ||||
|       Not an array: {typeof $currentValue} | ||||
|       {JSON.stringify(path)} | ||||
|       {JSON.stringify($currentValue).slice(0, 120)} | ||||
|     {:else if $currentValue?.length === 0} | ||||
|       No values are defined | ||||
|       {#if $messages.length > 0} | ||||
|         {#each $messages as message} | ||||
|           <ShowConversionMessage {message} /> | ||||
|         {/each} | ||||
|       {/if} | ||||
|     {:else if subparts.length === 0} | ||||
|       <!-- We need an array of values, so we use the typehint of the _parent_ element as field --> | ||||
|       {#each $currentValue as value, i} | ||||
|         <div class="flex w-full"> | ||||
|           <SchemaBasedField {state} {schema} path={fusePath(i)} /> | ||||
|           <button | ||||
|             class="h-fit w-fit rounded-full border border-black p-1" | ||||
|             on:click={() => { | ||||
|             del(i) | ||||
|           }} | ||||
|         > | ||||
|           <TrashIcon class="h-4 w-4" /> | ||||
|         </button> | ||||
|       </div> | ||||
|     {/each} | ||||
|   {:else} | ||||
|     <Accordion> | ||||
|       {#each $currentValue as value, i (value)} | ||||
|         <CollapsedTagRenderingPreview | ||||
|           {state} | ||||
|           {isTagRenderingBlock} | ||||
|           {schema} | ||||
|           {currentValue} | ||||
|           {value} | ||||
|           {i} | ||||
|           {singular} | ||||
|           path={fusePath(i)} | ||||
|         /> | ||||
|           > | ||||
|             <TrashIcon class="h-4 w-4" /> | ||||
|           </button> | ||||
|         </div> | ||||
|       {/each} | ||||
|     </Accordion> | ||||
|   {/if} | ||||
|   <div class="flex"> | ||||
|     <button on:click={() => createItem()}>Add {article} {singular}</button> | ||||
|     {#if path.length === 1 && path[0] === "tagRenderings"} | ||||
|       <button | ||||
|         on:click={() => { | ||||
|     {:else} | ||||
|       <Accordion> | ||||
|         {#each $currentValue as value, i} | ||||
|           <CollapsedTagRenderingPreview | ||||
|             {state} | ||||
|             {isTagRenderingBlock} | ||||
|             {schema} | ||||
|             {currentValue} | ||||
|             {value} | ||||
|             {i} | ||||
|             {singular} | ||||
|             path={fusePath(i)} | ||||
|           /> | ||||
|         {/each} | ||||
|       </Accordion> | ||||
|     {/if} | ||||
|     <div class="flex"> | ||||
|       <button on:click={() => createItem()}>Add {article} {singular}</button> | ||||
|       {#if path.length === 1 && path[0] === "tagRenderings"} | ||||
|         <button | ||||
|           on:click={() => { | ||||
|           createItem("images") | ||||
|         }} | ||||
|       > | ||||
|         Add a builtin tagRendering | ||||
|       </button> | ||||
|     {/if} | ||||
|     <slot name="extra-button" /> | ||||
|         > | ||||
|           Add a builtin tagRendering | ||||
|         </button> | ||||
|       {/if} | ||||
|       <slot name="extra-button" /> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ | |||
|   import ChevronRight from "@babeard/svelte-heroicons/mini/ChevronRight" | ||||
|   import ChevronLeft from "@babeard/svelte-heroicons/solid/ChevronLeft" | ||||
|   import { Drawer } from "flowbite-svelte" | ||||
|   import { linear, sineIn } from "svelte/easing" | ||||
|   import { linear } from "svelte/easing" | ||||
| 
 | ||||
|   export let state: ThemeViewState | ||||
| 
 | ||||
|  | @ -440,7 +440,6 @@ | |||
|       <MenuDrawer onlyLink={true} {state} /> | ||||
|     </div> | ||||
|   </DrawerLeft> | ||||
|   <MenuDrawer onlyLink={false} {state} /> | ||||
| 
 | ||||
|   {#if $selectedElement !== undefined && $selectedLayer !== undefined && !$selectedLayer.popupInFloatover} | ||||
|     <!-- right modal with the selected element view --> | ||||
|  | @ -494,4 +493,6 @@ | |||
|     {/if} | ||||
|   {/if} | ||||
| 
 | ||||
|   <MenuDrawer onlyLink={false} {state} /> | ||||
| 
 | ||||
| </main> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue