Studio: add slideshow, add useability tweaks
This commit is contained in:
parent
2df9aa8564
commit
8bc555fbe0
26 changed files with 440 additions and 316 deletions
|
@ -28,6 +28,18 @@ async function prepareFile(url: string): Promise<string> {
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
return fs.readFileSync(filePath, "utf8")
|
return fs.readFileSync(filePath, "utf8")
|
||||||
}
|
}
|
||||||
|
while (url.startsWith("/")) {
|
||||||
|
url = url.slice(1)
|
||||||
|
}
|
||||||
|
const sliced = url.split("/").slice(1)
|
||||||
|
if (!sliced) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const backupFile = path.join(STATIC_PATH, ...sliced)
|
||||||
|
console.log("Using bakcup path", backupFile)
|
||||||
|
if (fs.existsSync(backupFile)) {
|
||||||
|
return fs.readFileSync(backupFile, "utf8")
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +63,9 @@ http.createServer(async (req, res) => {
|
||||||
for (let i = 1; i < paths.length; i++) {
|
for (let i = 1; i < paths.length; i++) {
|
||||||
const p = paths.slice(0, i)
|
const p = paths.slice(0, i)
|
||||||
const dir = STATIC_PATH + p.join("/")
|
const dir = STATIC_PATH + p.join("/")
|
||||||
|
console.log("Checking if", dir, "exists...")
|
||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(dir)) {
|
||||||
|
console.log("Creating new directory", dir)
|
||||||
fs.mkdirSync(dir)
|
fs.mkdirSync(dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,22 +75,28 @@ http.createServer(async (req, res) => {
|
||||||
res.end()
|
res.end()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (req.url.endsWith("/overview")) {
|
|
||||||
|
const url = new URL(`http://127.0.0.1/` + req.url)
|
||||||
|
if (url.pathname.endsWith("overview")) {
|
||||||
console.log("Giving overview")
|
console.log("Giving overview")
|
||||||
|
let userId = url.searchParams.get("userId")
|
||||||
const allFiles = ScriptUtils.readDirRecSync(STATIC_PATH)
|
const allFiles = ScriptUtils.readDirRecSync(STATIC_PATH)
|
||||||
.filter((p) => p.endsWith(".json") && !p.endsWith("license_info.json"))
|
.filter(
|
||||||
|
(p) =>
|
||||||
|
p.endsWith(".json") &&
|
||||||
|
!p.endsWith("license_info.json") &&
|
||||||
|
(p.startsWith("layers") ||
|
||||||
|
p.startsWith("themes") ||
|
||||||
|
userId !== undefined ||
|
||||||
|
p.startsWith(userId))
|
||||||
|
)
|
||||||
.map((p) => p.substring(STATIC_PATH.length + 1))
|
.map((p) => p.substring(STATIC_PATH.length + 1))
|
||||||
res.writeHead(200, { "Content-Type": MIME_TYPES.json })
|
res.writeHead(200, { "Content-Type": MIME_TYPES.json })
|
||||||
res.write(JSON.stringify({ allFiles }))
|
res.write(JSON.stringify({ allFiles }))
|
||||||
res.end()
|
res.end()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!fs.existsSync(STATIC_PATH + req.url)) {
|
|
||||||
res.writeHead(404, { "Content-Type": MIME_TYPES.html })
|
|
||||||
res.write("<html><body><p>Not found...</p></body></html>")
|
|
||||||
res.end()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const file = await prepareFile(req.url)
|
const file = await prepareFile(req.url)
|
||||||
if (file === null) {
|
if (file === null) {
|
||||||
res.writeHead(404, { "Content-Type": MIME_TYPES.html })
|
res.writeHead(404, { "Content-Type": MIME_TYPES.html })
|
||||||
|
|
|
@ -435,7 +435,6 @@ class MappedStore<TIn, T> extends Store<T> {
|
||||||
* const mapped = src.map(i => i * 2)
|
* const mapped = src.map(i => i * 2)
|
||||||
* src.setData(3)
|
* src.setData(3)
|
||||||
* mapped.data // => 6
|
* mapped.data // => 6
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
get data(): T {
|
get data(): T {
|
||||||
if (!this._callbacksAreRegistered) {
|
if (!this._callbacksAreRegistered) {
|
||||||
|
|
|
@ -666,25 +666,29 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.freeform) {
|
if (json.freeform) {
|
||||||
const c = context.enters("freeform", "render")
|
|
||||||
if (json.render === undefined) {
|
if (json.render === undefined) {
|
||||||
c.err(
|
context
|
||||||
"This tagRendering allows to set a freeform, but does not define a way to `render` this value"
|
.enter("render")
|
||||||
|
.err(
|
||||||
|
"This tagRendering allows to set a value to key " +
|
||||||
|
json.freeform.key +
|
||||||
|
", but does not define a `render`. Please, add a value here which contains `{" +
|
||||||
|
json.freeform.key +
|
||||||
|
"}`"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const render = new Translation(<any>json.render)
|
const render = new Translation(<any>json.render)
|
||||||
|
|
||||||
for (const ln in render.translations) {
|
for (const ln in render.translations) {
|
||||||
if (ln.startsWith("_")) {
|
if (ln.startsWith("_")) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const txt: string = render.translations[ln]
|
const txt: string = render.translations[ln]
|
||||||
if (txt === "") {
|
if (txt === "") {
|
||||||
c.err(" Rendering for language " + ln + " is empty")
|
context.enter("render").err(" Rendering for language " + ln + " is empty")
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
txt.indexOf("{" + json.freeform.key + "}") >= 0 ||
|
txt.indexOf("{" + json.freeform.key + "}") >= 0 ||
|
||||||
txt.indexOf("&LBRACE" + json.freeform.key + "&RBRACE")
|
txt.indexOf("&LBRACE" + json.freeform.key + "&RBRACE") >= 0
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -721,8 +725,10 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
c.err(
|
context
|
||||||
`The rendering for language ${ln} does not contain the freeform key {${json.freeform.key}}. This is a bug, as this rendering should show exactly this freeform key!\nThe rendering is ${txt} `
|
.enter("render")
|
||||||
|
.err(
|
||||||
|
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. This is a bug, as this rendering should show exactly this freeform key!`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -783,7 +789,7 @@ export class ValidateLayer extends Conversion<
|
||||||
private readonly _path?: string
|
private readonly _path?: string
|
||||||
private readonly _isBuiltin: boolean
|
private readonly _isBuiltin: boolean
|
||||||
private readonly _doesImageExist: DoesImageExist
|
private readonly _doesImageExist: DoesImageExist
|
||||||
private _studioValidations: boolean
|
private readonly _studioValidations: boolean
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
path: string,
|
path: string,
|
||||||
|
@ -816,7 +822,7 @@ export class ValidateLayer extends Conversion<
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.id === undefined) {
|
if (json.id === undefined) {
|
||||||
context.err(`Not a valid layer: id is undefined: ${JSON.stringify(json)}`)
|
context.enter("id").err(`Not a valid layer: id is undefined`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.source === undefined) {
|
if (json.source === undefined) {
|
||||||
|
@ -922,6 +928,21 @@ export class ValidateLayer extends Conversion<
|
||||||
"Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set."
|
"Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Check for multiple, identical builtin questions - usability for studio users
|
||||||
|
const duplicates = Utils.Duplicates(
|
||||||
|
<string[]>json.tagRenderings.filter((tr) => typeof tr === "string")
|
||||||
|
)
|
||||||
|
for (let i = 0; i < json.tagRenderings.length; i++) {
|
||||||
|
const tagRendering = json.tagRenderings[i]
|
||||||
|
if (typeof tagRendering === "string" && duplicates.indexOf(tagRendering) > 0) {
|
||||||
|
context
|
||||||
|
.enters("tagRenderings", i)
|
||||||
|
.err(`This builtin question is used multiple times (${tagRendering})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json["builtin"] !== undefined) {
|
if (json["builtin"] !== undefined) {
|
||||||
|
|
|
@ -15,11 +15,18 @@ import { Translatable } from "./Translatable"
|
||||||
*/
|
*/
|
||||||
export interface LayerConfigJson {
|
export interface LayerConfigJson {
|
||||||
/**
|
/**
|
||||||
* The id of this layer.
|
|
||||||
* This should be a simple, lowercase, human readable string that is used to identify the layer.
|
|
||||||
*
|
|
||||||
* group: Basic
|
|
||||||
* question: What is the identifier of this layer?
|
* question: What is the identifier of this layer?
|
||||||
|
*
|
||||||
|
* This should be a simple, lowercase, human readable string that is used to identify the layer.
|
||||||
|
* A good ID is:
|
||||||
|
* - a noun
|
||||||
|
* - written in singular
|
||||||
|
* - describes the object
|
||||||
|
* - in english
|
||||||
|
* - only has lowercase letters, numbers or underscores. Do not use a space or a dash
|
||||||
|
*
|
||||||
|
* type: id
|
||||||
|
* group: Basic
|
||||||
*/
|
*/
|
||||||
id: string
|
id: string
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,7 @@
|
||||||
}
|
}
|
||||||
const apiState = state.osmConnection.apiIsOnline
|
const apiState = state.osmConnection.apiIsOnline
|
||||||
</script>
|
</script>
|
||||||
<slot />
|
|
||||||
<!--
|
|
||||||
{#if $badge}
|
{#if $badge}
|
||||||
{#if !ignoreLoading && $loadingStatus === "loading"}
|
{#if !ignoreLoading && $loadingStatus === "loading"}
|
||||||
<slot name="loading">
|
<slot name="loading">
|
||||||
|
@ -43,4 +42,4 @@
|
||||||
{:else if $loadingStatus === "not-attempted"}
|
{:else if $loadingStatus === "not-attempted"}
|
||||||
<slot name="not-logged-in" />
|
<slot name="not-logged-in" />
|
||||||
{/if}
|
{/if}
|
||||||
{/if} -->
|
{/if}
|
||||||
|
|
|
@ -22,7 +22,7 @@ $: documentation = TagUtils.modeDocumentation[mode];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<BasicTagInput bind:mode={mode} {dropdownFocussed} {overpassSupportNeeded} {silent} tag={value} {uploadableOnly} />
|
<BasicTagInput bind:mode={mode} {dropdownFocussed} {overpassSupportNeeded} {silent} tag={value} {uploadableOnly} on:submit />
|
||||||
{#if $dropdownFocussed}
|
{#if $dropdownFocussed}
|
||||||
<div class="border border-dashed border-black p-2 m-2">
|
<div class="border border-dashed border-black p-2 m-2">
|
||||||
<b>{documentation.name}</b>
|
<b>{documentation.name}</b>
|
||||||
|
|
|
@ -19,4 +19,4 @@ let tag: UIEventSource<string | TagConfigJson> = value
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} />
|
<FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} on:submit/>
|
||||||
|
|
|
@ -5,20 +5,14 @@
|
||||||
import { createEventDispatcher, onDestroy } from "svelte";
|
import { createEventDispatcher, onDestroy } from "svelte";
|
||||||
import ValidatedInput from "../ValidatedInput.svelte";
|
import ValidatedInput from "../ValidatedInput.svelte";
|
||||||
|
|
||||||
export let value: UIEventSource<string> = new UIEventSource<string>("");
|
export let value: UIEventSource<Record<string, string>> = new UIEventSource<Record<string, string>>({});
|
||||||
|
|
||||||
export let args: string[] = []
|
export let args: string[] = []
|
||||||
|
|
||||||
let prefix = args[0]
|
let prefix = args[0] ?? ""
|
||||||
let postfix = args[1]
|
let postfix = args[1] ?? ""
|
||||||
|
|
||||||
let translations: UIEventSource<Record<string, string>> = value.sync((s) => {
|
let translations: UIEventSource<Record<string, string>> = value
|
||||||
try {
|
|
||||||
return JSON.parse(s);
|
|
||||||
} catch (e) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}, [], v => JSON.stringify(v));
|
|
||||||
|
|
||||||
const allLanguages: string[] = LanguageUtils.usedLanguagesSorted;
|
const allLanguages: string[] = LanguageUtils.usedLanguagesSorted;
|
||||||
let currentLang = new UIEventSource("en");
|
let currentLang = new UIEventSource("en");
|
||||||
|
@ -28,6 +22,9 @@
|
||||||
function update() {
|
function update() {
|
||||||
const v = currentVal.data;
|
const v = currentVal.data;
|
||||||
const l = currentLang.data;
|
const l = currentLang.data;
|
||||||
|
if(translations.data === "" || translations.data === undefined){
|
||||||
|
translations.data = {}
|
||||||
|
}
|
||||||
if (translations.data[l] === v) {
|
if (translations.data[l] === v) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -37,6 +34,9 @@
|
||||||
|
|
||||||
onDestroy(currentLang.addCallbackAndRunD(currentLang => {
|
onDestroy(currentLang.addCallbackAndRunD(currentLang => {
|
||||||
console.log("Applying current lang:", currentLang);
|
console.log("Applying current lang:", currentLang);
|
||||||
|
if(!translations.data){
|
||||||
|
translations.data = {}
|
||||||
|
}
|
||||||
translations.data[currentLang] = translations.data[currentLang] ?? "";
|
translations.data[currentLang] = translations.data[currentLang] ?? "";
|
||||||
currentVal.setData(translations.data[currentLang]);
|
currentVal.setData(translations.data[currentLang]);
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -27,14 +27,13 @@
|
||||||
|
|
||||||
let properties = { feature, args: args ?? [] };
|
let properties = { feature, args: args ?? [] };
|
||||||
let dispatch = createEventDispatcher<{
|
let dispatch = createEventDispatcher<{
|
||||||
selected,
|
selected
|
||||||
submit
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if type === "translation" }
|
{#if type === "translation" }
|
||||||
<TranslationInput {value} on:submit={() => dispatch("submit")} {args} />
|
<TranslationInput {value} on:submit {args} />
|
||||||
{:else if type === "direction"}
|
{:else if type === "direction"}
|
||||||
<DirectionInput {value} mapProperties={InputHelpers.constructMapProperties(properties)} />
|
<DirectionInput {value} mapProperties={InputHelpers.constructMapProperties(properties)} />
|
||||||
{:else if type === "date"}
|
{:else if type === "date"}
|
||||||
|
@ -44,9 +43,9 @@
|
||||||
{:else if type === "image"}
|
{:else if type === "image"}
|
||||||
<ImageHelper { value } />
|
<ImageHelper { value } />
|
||||||
{:else if type === "tag"}
|
{:else if type === "tag"}
|
||||||
<TagInput { value } />
|
<TagInput { value } on:submit />
|
||||||
{:else if type === "simple_tag"}
|
{:else if type === "simple_tag"}
|
||||||
<SimpleTagInput { value } {args} />
|
<SimpleTagInput { value } {args} on:submit />
|
||||||
{:else if type === "opening_hours"}
|
{:else if type === "opening_hours"}
|
||||||
<OpeningHoursInput { value } />
|
<OpeningHoursInput { value } />
|
||||||
{:else if type === "wikidata"}
|
{:else if type === "wikidata"}
|
||||||
|
|
|
@ -109,7 +109,7 @@
|
||||||
* Dispatches the submit, but only if the value is valid
|
* Dispatches the submit, but only if the value is valid
|
||||||
*/
|
*/
|
||||||
function sendSubmit(){
|
function sendSubmit(){
|
||||||
if(feedback.data){
|
if(feedback?.data){
|
||||||
console.log("Not sending a submit as there is feedback")
|
console.log("Not sending a submit as there is feedback")
|
||||||
}
|
}
|
||||||
dispatch("submit")
|
dispatch("submit")
|
||||||
|
|
|
@ -259,7 +259,6 @@
|
||||||
value={freeformInput}
|
value={freeformInput}
|
||||||
on:selected={() => (selectedMapping = config.mappings?.length)}
|
on:selected={() => (selectedMapping = config.mappings?.length)}
|
||||||
on:submit={onSave}
|
on:submit={onSave}
|
||||||
submit={onSave}
|
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1336,6 +1336,7 @@ export default class SpecialVisualizations {
|
||||||
const tr = typeof v === "string" ? JSON.parse(v) : v
|
const tr = typeof v === "string" ? JSON.parse(v) : v
|
||||||
return new Translation(tr).SetClass("font-bold")
|
return new Translation(tr).SetClass("font-bold")
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error("Cannot create a translation for", v, "due to", e)
|
||||||
return JSON.stringify(v)
|
return JSON.stringify(v)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Marker from "../Map/Marker.svelte";
|
||||||
|
import NextButton from "../Base/NextButton.svelte";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import { AllSharedLayers } from "../../Customizations/AllSharedLayers";
|
||||||
|
|
||||||
|
export let layerIds : { id: string }[]
|
||||||
|
const dispatch = createEventDispatcher<{layerSelected: string}>()
|
||||||
|
|
||||||
|
function fetchIconDescription(layerId): any {
|
||||||
|
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if layerIds.length > 0}
|
||||||
|
<slot name="title"/>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
{#each Array.from(layerIds) as layer}
|
||||||
|
<NextButton clss="small" on:click={() => dispatch("layerSelected", layer.id)}>
|
||||||
|
<div class="w-4 h-4 mr-1">
|
||||||
|
<Marker icons={fetchIconDescription(layer.id)} />
|
||||||
|
</div>
|
||||||
|
{layer.id}
|
||||||
|
</NextButton>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -11,6 +11,7 @@
|
||||||
import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion";
|
import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion";
|
||||||
import ErrorIndicatorForRegion from "./ErrorIndicatorForRegion.svelte";
|
import ErrorIndicatorForRegion from "./ErrorIndicatorForRegion.svelte";
|
||||||
import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid";
|
import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||||
|
import SchemaBasedInput from "./SchemaBasedInput.svelte";
|
||||||
|
|
||||||
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
|
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
|
||||||
|
|
||||||
|
@ -25,7 +26,6 @@
|
||||||
* Blacklist of regions for the general area tab
|
* Blacklist of regions for the general area tab
|
||||||
* These are regions which are handled by a different tab
|
* These are regions which are handled by a different tab
|
||||||
*/
|
*/
|
||||||
const regionBlacklist = ["hidden", undefined, "infobox", "tagrenderings", "maprendering", "editing", "title", "linerendering", "pointrendering"];
|
|
||||||
const allNames = Utils.Dedup(layerSchema.map(meta => meta.hints.group));
|
const allNames = Utils.Dedup(layerSchema.map(meta => meta.hints.group));
|
||||||
|
|
||||||
const perRegion: Record<string, ConfigMeta[]> = {};
|
const perRegion: Record<string, ConfigMeta[]> = {};
|
||||||
|
@ -33,12 +33,7 @@
|
||||||
perRegion[region] = layerSchema.filter(meta => meta.hints.group === region);
|
perRegion[region] = layerSchema.filter(meta => meta.hints.group === region);
|
||||||
}
|
}
|
||||||
|
|
||||||
const baselayerRegions: string[] = ["Basic", "presets", "filters"];
|
|
||||||
for (const baselayerRegion of baselayerRegions) {
|
|
||||||
if (perRegion[baselayerRegion] === undefined) {
|
|
||||||
console.error("BaseLayerRegions in editLayer: no items have group '" + baselayerRegion + "\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const title: Store<string> = state.getStoreFor(["id"]);
|
const title: Store<string> = state.getStoreFor(["id"]);
|
||||||
const wl = window.location;
|
const wl = window.location;
|
||||||
const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout=";
|
const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout=";
|
||||||
|
@ -46,18 +41,46 @@
|
||||||
function firstPathsFor(...regionNames: string[]): Set<string> {
|
function firstPathsFor(...regionNames: string[]): Set<string> {
|
||||||
const pathNames = new Set<string>();
|
const pathNames = new Set<string>();
|
||||||
for (const regionName of regionNames) {
|
for (const regionName of regionNames) {
|
||||||
const region: ConfigMeta[] = perRegion[regionName]
|
const region: ConfigMeta[] = perRegion[regionName];
|
||||||
for (const configMeta of region) {
|
for (const configMeta of region) {
|
||||||
pathNames.add(configMeta.path[0])
|
pathNames.add(configMeta.path[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pathNames;
|
return pathNames;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function configForRequiredField(id: string): ConfigMeta{
|
||||||
|
let config = layerSchema.find(config => config.path.length === 1 && config.path[0] === id)
|
||||||
|
config = Utils.Clone(config)
|
||||||
|
config.required = true
|
||||||
|
console.log(">>>", config)
|
||||||
|
config.hints.ifunset = undefined
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
let requiredFields = ["id", "name", "description"];
|
||||||
|
let currentlyMissing = state.configuration.map(config => {
|
||||||
|
const missing = [];
|
||||||
|
for (const requiredField of requiredFields) {
|
||||||
|
if (!config[requiredField]) {
|
||||||
|
missing.push(requiredField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return missing;
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full flex justify-between">
|
{#if $currentlyMissing.length > 0}
|
||||||
|
|
||||||
|
{#each requiredFields as required}
|
||||||
|
<SchemaBasedInput {state}
|
||||||
|
schema={configForRequiredField(required)}
|
||||||
|
path={[required]} />
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<div class="w-full flex justify-between my-2">
|
||||||
<slot />
|
<slot />
|
||||||
<h3>Editing layer {$title}</h3>
|
<h3>Editing layer {$title}</h3>
|
||||||
{#if $hasErrors > 0}
|
{#if $hasErrors > 0}
|
||||||
|
@ -65,46 +88,59 @@
|
||||||
{:else}
|
{:else}
|
||||||
<a class="primary button" href={baseUrl+state.server.layerUrl(title.data)} target="_blank" rel="noopener">
|
<a class="primary button" href={baseUrl+state.server.layerUrl(title.data)} target="_blank" rel="noopener">
|
||||||
Try it out
|
Try it out
|
||||||
<ChevronRightIcon class= "h-6 w-6 shrink-0"/>
|
<ChevronRightIcon class="h-6 w-6 shrink-0" />
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="m4">
|
<div class="m4">
|
||||||
<TabbedGroup>
|
<TabbedGroup>
|
||||||
<div slot="title0" class="flex">General properties
|
<div slot="title0" class="flex">General properties
|
||||||
<ErrorIndicatorForRegion firstPaths={firstPathsFor(...baselayerRegions)} {state} />
|
<ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col" slot="content0">
|
<div class="flex flex-col" slot="content0">
|
||||||
{#each baselayerRegions as region}
|
<Region {state} configs={perRegion["Basic"]} />
|
||||||
<Region {state} configs={perRegion[region]} title={region} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div slot="title1" class="flex">Information panel (questions and answers)
|
<div slot="title1" class="flex">Information panel (questions and answers)
|
||||||
<ErrorIndicatorForRegion firstPaths={firstPathsFor("title","tagrenderings","editing")} {state} /></div>
|
<ErrorIndicatorForRegion firstPaths={firstPathsFor("title","tagrenderings","editing")} {state} />
|
||||||
|
</div>
|
||||||
<div slot="content1">
|
<div slot="content1">
|
||||||
<Region configs={perRegion["title"]} {state} title="Popup title" />
|
<Region configs={perRegion["title"]} {state} title="Popup title" />
|
||||||
<Region configs={perRegion["tagrenderings"]} {state} title="Popup contents" />
|
<Region configs={perRegion["tagrenderings"]} {state} title="Popup contents" />
|
||||||
<Region configs={perRegion["editing"]} {state} title="Other editing elements" />
|
<Region configs={perRegion["editing"]} {state} title="Other editing elements" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div slot="title2" class="flex">Rendering on the map
|
<div slot="title2">
|
||||||
<ErrorIndicatorForRegion firstPaths={firstPathsFor("linerendering","pointrendering")} {state} /></div>
|
<ErrorIndicatorForRegion firstPaths={firstPathsFor("presets")} {state} />
|
||||||
|
Creating a new point
|
||||||
|
</div>
|
||||||
|
|
||||||
<div slot="content2">
|
<div slot="content2">
|
||||||
|
<Region {state} configs={perRegion["presets"]} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div slot="title3" class="flex">Rendering on the map
|
||||||
|
<ErrorIndicatorForRegion firstPaths={firstPathsFor("linerendering","pointrendering")} {state} />
|
||||||
|
</div>
|
||||||
|
<div slot="content3">
|
||||||
<Region configs={perRegion["linerendering"]} {state} />
|
<Region configs={perRegion["linerendering"]} {state} />
|
||||||
<Region configs={perRegion["pointrendering"]} {state} />
|
<Region configs={perRegion["pointrendering"]} {state} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div slot="title3" class="flex">Advanced functionality
|
<div slot="title4" class="flex">Advanced functionality
|
||||||
<ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced","expert")} {state} /></div>
|
<ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced","expert")} {state} />
|
||||||
<div slot="content3">
|
</div>
|
||||||
|
<div slot="content4">
|
||||||
<Region configs={perRegion["advanced"]} {state} />
|
<Region configs={perRegion["advanced"]} {state} />
|
||||||
<Region configs={perRegion["expert"]} {state} />
|
<Region configs={perRegion["expert"]} {state} />
|
||||||
</div>
|
</div>
|
||||||
<div slot="title4">Configuration file</div>
|
<div slot="title5">Configuration file</div>
|
||||||
<div slot="content4">
|
<div slot="content5">
|
||||||
<div>
|
<div>
|
||||||
Below, you'll find the raw configuration file in `.json`-format.
|
Below, you'll find the raw configuration file in `.json`-format.
|
||||||
This is mostly for debugging purposes
|
This is mosSendertly for debugging purposes
|
||||||
</div>
|
</div>
|
||||||
<div class="literal-code">
|
<div class="literal-code">
|
||||||
{JSON.stringify($configuration, null, " ")}
|
{JSON.stringify($configuration, null, " ")}
|
||||||
|
@ -122,4 +158,5 @@
|
||||||
</div>
|
</div>
|
||||||
</TabbedGroup>
|
</TabbedGroup>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
|
||||||
import { ConfigMeta } from "./configMeta"
|
import { ConfigMeta } from "./configMeta"
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||||
import { QueryParameters } from "../../Logic/Web/QueryParameters"
|
|
||||||
import {
|
import {
|
||||||
ConversionContext,
|
ConversionContext,
|
||||||
ConversionMessage,
|
ConversionMessage,
|
||||||
|
@ -16,25 +14,29 @@ import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Jso
|
||||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||||
import StudioServer from "./StudioServer"
|
import StudioServer from "./StudioServer"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
|
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends changes back to the server
|
* Sends changes back to the server
|
||||||
*/
|
*/
|
||||||
export class LayerStateSender {
|
export class LayerStateSender {
|
||||||
constructor(layerState: EditLayerState) {
|
constructor(layerState: EditLayerState) {
|
||||||
layerState.configuration.addCallback(async (config) => {
|
const layerId = layerState.configuration.map((config) => config.id)
|
||||||
const id = config.id
|
layerState.configuration
|
||||||
|
.mapD((config) => JSON.stringify(config, null, " "))
|
||||||
|
.stabilized(100)
|
||||||
|
.addCallbackD(async (config) => {
|
||||||
|
const id = layerId.data
|
||||||
if (id === undefined) {
|
if (id === undefined) {
|
||||||
console.warn("No id found in layer, not updating")
|
console.warn("No id found in layer, not updating")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await layerState.server.updateLayer(<LayerConfigJson>config)
|
await layerState.server.updateLayer(id, config)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class EditLayerState {
|
export default class EditLayerState {
|
||||||
public readonly osmConnection: OsmConnection
|
|
||||||
public readonly schema: ConfigMeta[]
|
public readonly schema: ConfigMeta[]
|
||||||
|
|
||||||
public readonly featureSwitches: { featureSwitchIsDebugging: UIEventSource<boolean> }
|
public readonly featureSwitches: { featureSwitchIsDebugging: UIEventSource<boolean> }
|
||||||
|
@ -44,17 +46,14 @@ export default class EditLayerState {
|
||||||
>({})
|
>({})
|
||||||
public readonly messages: Store<ConversionMessage[]>
|
public readonly messages: Store<ConversionMessage[]>
|
||||||
public readonly server: StudioServer
|
public readonly server: StudioServer
|
||||||
|
// Needed for the special visualisations
|
||||||
|
public readonly osmConnection: OsmConnection
|
||||||
|
private readonly _stores = new Map<string, UIEventSource<any>>()
|
||||||
|
|
||||||
constructor(schema: ConfigMeta[], server: StudioServer) {
|
constructor(schema: ConfigMeta[], server: StudioServer, osmConnection: OsmConnection) {
|
||||||
this.schema = schema
|
this.schema = schema
|
||||||
this.server = server
|
this.server = server
|
||||||
this.osmConnection = new OsmConnection({
|
this.osmConnection = osmConnection
|
||||||
oauth_token: QueryParameters.GetQueryParameter(
|
|
||||||
"oauth_token",
|
|
||||||
undefined,
|
|
||||||
"Used to complete the login"
|
|
||||||
),
|
|
||||||
})
|
|
||||||
this.featureSwitches = {
|
this.featureSwitches = {
|
||||||
featureSwitchIsDebugging: new UIEventSource<boolean>(true),
|
featureSwitchIsDebugging: new UIEventSource<boolean>(true),
|
||||||
}
|
}
|
||||||
|
@ -118,7 +117,6 @@ export default class EditLayerState {
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly _stores = new Map<string, UIEventSource<any>>()
|
|
||||||
public getStoreFor<T>(path: ReadonlyArray<string | number>): UIEventSource<T | undefined> {
|
public getStoreFor<T>(path: ReadonlyArray<string | number>): UIEventSource<T | undefined> {
|
||||||
const key = path.join(".")
|
const key = path.join(".")
|
||||||
|
|
||||||
|
@ -139,7 +137,9 @@ export default class EditLayerState {
|
||||||
value: Store<any>,
|
value: Store<any>,
|
||||||
noInitialSync: boolean = false
|
noInitialSync: boolean = false
|
||||||
): () => void {
|
): () => void {
|
||||||
const unsync = value.addCallback((v) => this.setValueAt(path, v))
|
const unsync = value.addCallback((v) => {
|
||||||
|
this.setValueAt(path, v)
|
||||||
|
})
|
||||||
if (!noInitialSync) {
|
if (!noInitialSync) {
|
||||||
this.setValueAt(path, value.data)
|
this.setValueAt(path, value.data)
|
||||||
}
|
}
|
||||||
|
@ -180,6 +180,7 @@ export default class EditLayerState {
|
||||||
|
|
||||||
public setValueAt(path: ReadonlyArray<string | number>, v: any) {
|
public setValueAt(path: ReadonlyArray<string | number>, v: any) {
|
||||||
let entry = this.configuration.data
|
let entry = this.configuration.data
|
||||||
|
console.log("Setting value at", path, v)
|
||||||
const isUndefined =
|
const isUndefined =
|
||||||
v === undefined ||
|
v === undefined ||
|
||||||
v === null ||
|
v === null ||
|
||||||
|
@ -197,15 +198,35 @@ export default class EditLayerState {
|
||||||
}
|
}
|
||||||
entry = entry[breadcrumb]
|
entry = entry[breadcrumb]
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastBreadcrumb = path.at(-1)
|
const lastBreadcrumb = path.at(-1)
|
||||||
if (isUndefined) {
|
if (isUndefined) {
|
||||||
if (entry && entry[lastBreadcrumb]) {
|
if (entry && entry[lastBreadcrumb]) {
|
||||||
console.log("Deleting", lastBreadcrumb, "of", path.join("."))
|
console.log("Deleting", lastBreadcrumb, "of", path.join("."))
|
||||||
delete entry[lastBreadcrumb]
|
delete entry[lastBreadcrumb]
|
||||||
}
|
|
||||||
} else {
|
|
||||||
entry[lastBreadcrumb] = v
|
|
||||||
}
|
|
||||||
this.configuration.ping()
|
this.configuration.ping()
|
||||||
}
|
}
|
||||||
|
} else if (entry[lastBreadcrumb] !== v) {
|
||||||
|
console.log("Assigning and pinging at", path)
|
||||||
|
entry[lastBreadcrumb] = v
|
||||||
|
this.configuration.ping()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public messagesFor(path: ReadonlyArray<string | number>): Store<ConversionMessage[]> {
|
||||||
|
return this.messages.map((msgs) => {
|
||||||
|
if (!msgs) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return msgs.filter((msg) => {
|
||||||
|
const pth = msg.context.path
|
||||||
|
for (let i = 0; i < Math.min(pth.length, path.length); i++) {
|
||||||
|
if (pth[i] !== path[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,7 +110,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
<div class="border border-black">
|
<div class="border border-black">
|
||||||
{#if isTagRenderingBlock}
|
{#if isTagRenderingBlock}
|
||||||
<TagRenderingInput path={path.concat(value)} {state} {schema} >
|
<TagRenderingInput path={[...path, (value)]} {state} {schema} >
|
||||||
<button slot="upper-right" class="border-black border rounded-full p-1 w-fit h-fit"
|
<button slot="upper-right" class="border-black border rounded-full p-1 w-fit h-fit"
|
||||||
on:click={() => {del(value)}}>
|
on:click={() => {del(value)}}>
|
||||||
<TrashIcon class="w-4 h-4" />
|
<TrashIcon class="w-4 h-4" />
|
||||||
|
|
|
@ -21,12 +21,12 @@
|
||||||
const isTranslation = schema.hints.typehint === "translation" || schema.hints.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema);
|
const isTranslation = schema.hints.typehint === "translation" || schema.hints.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema);
|
||||||
let type = schema.hints.typehint ?? "string";
|
let type = schema.hints.typehint ?? "string";
|
||||||
|
|
||||||
let rendervalue = ((schema.hints.inline ?? schema.path.join(".")) + " <b>{translated(value)}</b>");
|
let rendervalue = (schema.hints.inline ?? schema.path.join(".")) + (isTranslation ? " <b>{translated(value)}</b>": " <b>{value}</b>");
|
||||||
|
|
||||||
if(schema.type === "boolean"){
|
if(schema.type === "boolean"){
|
||||||
rendervalue = undefined
|
rendervalue = undefined
|
||||||
}
|
}
|
||||||
if(schema.hints.typehint === "tag") {
|
if(schema.hints.typehint === "tag" || schema.hints.typehint === "simple_tag") {
|
||||||
rendervalue = "{tags()}"
|
rendervalue = "{tags()}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,12 +61,12 @@
|
||||||
if (schema.hints.default) {
|
if (schema.hints.default) {
|
||||||
configJson.mappings = [{
|
configJson.mappings = [{
|
||||||
if: "value=", // We leave this blank
|
if: "value=", // We leave this blank
|
||||||
then: schema.path.at(-1) + " is not set. The default value <b>" + schema.hints.default + "</b> will be used. " + (schema.hints.ifunset ?? "")
|
then: path.at(-1) + " is not set. The default value <b>" + schema.hints.default + "</b> will be used. " + (schema.hints.ifunset ?? "")
|
||||||
}];
|
}];
|
||||||
} else if (!schema.required) {
|
} else if (!schema.required) {
|
||||||
configJson.mappings = [{
|
configJson.mappings = [{
|
||||||
if: "value=",
|
if: "value=",
|
||||||
then: schema.path.at(-1) + " is not set. " + (schema.hints.ifunset ?? "")
|
then: path.at(-1) + " is not set. " + (schema.hints.ifunset ?? "")
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,15 +109,7 @@
|
||||||
}
|
}
|
||||||
let config: TagRenderingConfig;
|
let config: TagRenderingConfig;
|
||||||
let err: string = undefined;
|
let err: string = undefined;
|
||||||
let messages = state.messages.mapD(msgs => msgs.filter(msg => {
|
let messages = state.messagesFor(path)
|
||||||
const pth = msg.context.path;
|
|
||||||
for (let i = 0; i < Math.min(pth.length, path.length); i++) {
|
|
||||||
if (pth[i] !== path[i]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}));
|
|
||||||
try {
|
try {
|
||||||
config = new TagRenderingConfig(configJson, "config based on " + schema.path.join("."));
|
config = new TagRenderingConfig(configJson, "config based on " + schema.path.join("."));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -130,7 +122,7 @@
|
||||||
onDestroy(state.register(path, tags.map(tgs => {
|
onDestroy(state.register(path, tags.map(tgs => {
|
||||||
const v = tgs["value"];
|
const v = tgs["value"];
|
||||||
if (typeof v !== "string") {
|
if (typeof v !== "string") {
|
||||||
return v;
|
return { ... v };
|
||||||
}
|
}
|
||||||
if (schema.type === "boolan") {
|
if (schema.type === "boolan") {
|
||||||
return v === "true" || v === "yes" || v === "1";
|
return v === "true" || v === "yes" || v === "1";
|
||||||
|
@ -140,7 +132,6 @@
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (v === "false" || v === "no" || v === "0") {
|
if (v === "false" || v === "no" || v === "0") {
|
||||||
console.log("Setting false...");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,7 +164,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{#if window.location.hostname === "127.0.0.1"}
|
{#if window.location.hostname === "127.0.0.1"}
|
||||||
<span class="subtle">{schema.path.join(".")} {schema.hints.typehint}</span>
|
<span class="subtle">{path.join(".")} {schema.hints.typehint}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
{#if schema.hints.typehint === "tagrendering[]"}
|
{#if schema.hints.typehint === "tagrendering[]"}
|
||||||
<!-- We cheat a bit here by matching this 'magical' type... -->
|
<!-- We cheat a bit here by matching this 'magical' type... -->
|
||||||
<SchemaBasedArray {path} {state} {schema} />
|
<SchemaBasedArray {path} {state} {schema} />
|
||||||
{:else if schema.type === "array" && schema.hints.multianswer === "true"}
|
{:else if schema.type === "array" && schema.hints.multianswer === "true"}
|
||||||
<ArrayMultiAnswer {path} {state} {schema}/>
|
<ArrayMultiAnswer {path} {state} {schema}/>
|
||||||
{:else if schema.type === "array"}
|
{:else if schema.type === "array"}
|
||||||
<SchemaBasedArray {path} {state} {schema} />
|
<SchemaBasedArray {path} {state} {schema} />
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
import type { JsonSchemaType } from "./jsonSchema";
|
import type { JsonSchemaType } from "./jsonSchema";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import nmd from "nano-markdown";
|
import nmd from "nano-markdown";
|
||||||
import { writable } from "svelte/store";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If 'types' is defined: allow the user to pick one of the types to input.
|
* If 'types' is defined: allow the user to pick one of the types to input.
|
||||||
|
@ -42,7 +41,7 @@
|
||||||
}
|
}
|
||||||
const configJson: QuestionableTagRenderingConfigJson = {
|
const configJson: QuestionableTagRenderingConfigJson = {
|
||||||
id: "TYPE_OF:" + path.join("_"),
|
id: "TYPE_OF:" + path.join("_"),
|
||||||
question: "Which subcategory is needed for "+schema.path.at(-1)+"?",
|
question: "Which subcategory is needed for " + schema.path.at(-1) + "?",
|
||||||
questionHint: nmd(schema.description),
|
questionHint: nmd(schema.description),
|
||||||
mappings: types.map(opt => opt.trim()).filter(opt => opt.length > 0).map((opt, i) => ({
|
mappings: types.map(opt => opt.trim()).filter(opt => opt.length > 0).map((opt, i) => ({
|
||||||
if: "chosen_type_index=" + i,
|
if: "chosen_type_index=" + i,
|
||||||
|
@ -127,14 +126,14 @@
|
||||||
possibleTypes.sort((a, b) => b.optionalMatches - a.optionalMatches);
|
possibleTypes.sort((a, b) => b.optionalMatches - a.optionalMatches);
|
||||||
possibleTypes.sort((a, b) => b.matchingPropertiesCount - a.matchingPropertiesCount);
|
possibleTypes.sort((a, b) => b.matchingPropertiesCount - a.matchingPropertiesCount);
|
||||||
if (possibleTypes.length > 0) {
|
if (possibleTypes.length > 0) {
|
||||||
chosenOption = possibleTypes[0].index
|
chosenOption = possibleTypes[0].index;
|
||||||
tags.setData({ chosen_type_index: "" + chosenOption});
|
tags.setData({ chosen_type_index: "" + chosenOption });
|
||||||
|
|
||||||
}
|
}
|
||||||
} else if (defaultOption !== undefined) {
|
} else if (defaultOption !== undefined) {
|
||||||
tags.setData({ chosen_type_index: "" + defaultOption });
|
tags.setData({ chosen_type_index: "" + defaultOption });
|
||||||
}else{
|
} else {
|
||||||
chosenOption = defaultOption
|
chosenOption = defaultOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasBooleanOption >= 0 || lastIsString) {
|
if (hasBooleanOption >= 0 || lastIsString) {
|
||||||
|
@ -154,7 +153,7 @@
|
||||||
let subSchemas: ConfigMeta[] = [];
|
let subSchemas: ConfigMeta[] = [];
|
||||||
|
|
||||||
let subpath = path;
|
let subpath = path;
|
||||||
const store = state.getStoreFor(path)
|
const store = state.getStoreFor(path);
|
||||||
onDestroy(tags.addCallbackAndRun(tags => {
|
onDestroy(tags.addCallbackAndRun(tags => {
|
||||||
if (tags["value"] !== undefined && tags["value"] !== "") {
|
if (tags["value"] !== undefined && tags["value"] !== "") {
|
||||||
chosenOption = undefined;
|
chosenOption = undefined;
|
||||||
|
@ -170,7 +169,7 @@
|
||||||
for (const key of type?.required ?? []) {
|
for (const key of type?.required ?? []) {
|
||||||
o[key] ??= {};
|
o[key] ??= {};
|
||||||
}
|
}
|
||||||
store.setData(o)
|
store.setData(o);
|
||||||
}
|
}
|
||||||
if (!type) {
|
if (!type) {
|
||||||
return;
|
return;
|
||||||
|
@ -191,6 +190,7 @@
|
||||||
subSchemas.push(...(state.getSchema([...cleanPath, crumble])));
|
subSchemas.push(...(state.getSchema([...cleanPath, crumble])));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
let messages = state.messagesFor(path);
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -209,5 +209,9 @@
|
||||||
<SchemaBasedInput {state} schema={subschema}
|
<SchemaBasedInput {state} schema={subschema}
|
||||||
path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput>
|
path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput>
|
||||||
{/each}
|
{/each}
|
||||||
|
{:else if $messages.length > 0}
|
||||||
|
{#each $messages as msg}
|
||||||
|
<div class="alert">{msg.message}</div>
|
||||||
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
export let path: (string | number)[] = [];
|
export let path: (string | number)[] = [];
|
||||||
export let schema: ConfigMeta;
|
export let schema: ConfigMeta;
|
||||||
|
|
||||||
let value = new UIEventSource<string>("{}");
|
let value = new UIEventSource<string>({});
|
||||||
console.log("Registering translation to path", path)
|
console.log("Registering translation to path", path)
|
||||||
state.register(path, value.mapD(v => JSON.parse(value.data )));
|
state.register(path, value.mapD(v => JSON.parse(value.data )));
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,23 +1,51 @@
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants"
|
||||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||||
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
|
|
||||||
export default class StudioServer {
|
export default class StudioServer {
|
||||||
private readonly url: string
|
private readonly url: string
|
||||||
|
private readonly _userId: Store<number>
|
||||||
|
|
||||||
constructor(url: string) {
|
constructor(url: string, userId: Store<number>) {
|
||||||
this.url = url
|
this.url = url
|
||||||
|
this._userId = userId
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetchLayerOverview(): Promise<Set<string>> {
|
public async fetchLayerOverview(): Promise<
|
||||||
|
{
|
||||||
|
id: string
|
||||||
|
owner: number
|
||||||
|
}[]
|
||||||
|
> {
|
||||||
|
const uid = this._userId.data
|
||||||
|
let uidQueryParam = ""
|
||||||
|
if (this._userId.data !== undefined) {
|
||||||
|
uidQueryParam = "?userId=" + uid
|
||||||
|
}
|
||||||
const { allFiles } = <{ allFiles: string[] }>(
|
const { allFiles } = <{ allFiles: string[] }>(
|
||||||
await Utils.downloadJson(this.url + "/overview")
|
await Utils.downloadJson(this.url + "/overview" + uidQueryParam)
|
||||||
)
|
)
|
||||||
const layers = allFiles
|
const layerOverview: {
|
||||||
.filter((f) => f.startsWith("layers/"))
|
id: string
|
||||||
.map((l) => l.substring(l.lastIndexOf("/") + 1, l.length - ".json".length))
|
owner: number | undefined
|
||||||
.filter((layerId) => Constants.priviliged_layers.indexOf(<any>layerId) < 0)
|
}[] = []
|
||||||
return new Set<string>(layers)
|
for (let file of allFiles) {
|
||||||
|
let owner = undefined
|
||||||
|
if (file.startsWith("" + uid)) {
|
||||||
|
owner = uid
|
||||||
|
file = file.substring(file.indexOf("/") + 1)
|
||||||
|
}
|
||||||
|
if (!file.startsWith("layers/")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const id = file.substring(file.lastIndexOf("/") + 1, file.length - ".json".length)
|
||||||
|
if (Constants.priviliged_layers.indexOf(<any>id) > 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
layerOverview.push({ id, owner })
|
||||||
|
}
|
||||||
|
return layerOverview
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchLayer(layerId: string): Promise<LayerConfigJson> {
|
async fetchLayer(layerId: string): Promise<LayerConfigJson> {
|
||||||
|
@ -28,8 +56,7 @@ export default class StudioServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateLayer(config: LayerConfigJson) {
|
async updateLayer(id: string, config: string) {
|
||||||
const id = config.id
|
|
||||||
if (id === undefined || id === "") {
|
if (id === undefined || id === "") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -38,11 +65,13 @@ export default class StudioServer {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json;charset=utf-8",
|
"Content-Type": "application/json;charset=utf-8",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(config, null, " "),
|
body: config,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public layerUrl(id: string) {
|
public layerUrl(id: string) {
|
||||||
return `${this.url}/layers/${id}/${id}.json`
|
const uid = this._userId.data
|
||||||
|
const uidStr = uid !== undefined ? "/" + uid : ""
|
||||||
|
return `${this.url}${uidStr}/layers/${id}/${id}.json`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,7 +139,7 @@ if (!initialTag) {
|
||||||
<div class="border-l-4 border-black flex flex-col ml-1 pl-1">
|
<div class="border-l-4 border-black flex flex-col ml-1 pl-1">
|
||||||
{#each $basicTags as basicTag (basicTag)}
|
{#each $basicTags as basicTag (basicTag)}
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} />
|
<BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} on:submit />
|
||||||
{#if $basicTags.length + $expressions.length > 1}
|
{#if $basicTags.length + $expressions.length > 1}
|
||||||
<button class="border border-black rounded-full w-fit h-fit p-0"
|
<button class="border border-black rounded-full w-fit h-fit p-0"
|
||||||
on:click={() => removeTag(basicTag)}>
|
on:click={() => removeTag(basicTag)}>
|
||||||
|
|
|
@ -111,7 +111,7 @@
|
||||||
<div class="flex h-fit ">
|
<div class="flex h-fit ">
|
||||||
|
|
||||||
<ValidatedInput feedback={feedbackKey} placeholder="The key of the tag" type="key"
|
<ValidatedInput feedback={feedbackKey} placeholder="The key of the tag" type="key"
|
||||||
value={keyValue}></ValidatedInput>
|
value={keyValue} on:submit></ValidatedInput>
|
||||||
<select bind:value={mode} on:focusin={() => dropdownFocussed.setData(true)} on:focusout={() => dropdownFocussed.setData(false)}>
|
<select bind:value={mode} on:focusin={() => dropdownFocussed.setData(true)} on:focusout={() => dropdownFocussed.setData(false)}>
|
||||||
{#each modes as option}
|
{#each modes as option}
|
||||||
<option value={option}>
|
<option value={option}>
|
||||||
|
@ -120,7 +120,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<ValidatedInput feedback={feedbackValue} placeholder="The value of the tag" type="string"
|
<ValidatedInput feedback={feedbackValue} placeholder="The value of the tag" type="string"
|
||||||
value={valueValue}></ValidatedInput>
|
value={valueValue} on:submit></ValidatedInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $feedbackKey}
|
{#if $feedbackKey}
|
||||||
|
|
|
@ -13,7 +13,7 @@ export let silent: boolean
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="m-2">
|
<div class="m-2">
|
||||||
<TagExpression {silent} {overpassSupportNeeded} {tag} {uploadableOnly}>
|
<TagExpression {silent} {overpassSupportNeeded} {tag} {uploadableOnly} on:submit>
|
||||||
<slot name="delete" slot="delete"/>
|
<slot name="delete" slot="delete"/>
|
||||||
</TagExpression>
|
</TagExpression>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -58,7 +58,6 @@ tags.addCallbackAndRunD(tgs => {
|
||||||
|
|
||||||
let mappings: UIEventSource<MappingConfigJson[]> = state.getStoreFor([...path, "mappings"]);
|
let mappings: UIEventSource<MappingConfigJson[]> = state.getStoreFor([...path, "mappings"]);
|
||||||
|
|
||||||
$: console.log("Allow questions:", $allowQuestions)
|
|
||||||
const topLevelItems: Record<string, ConfigMeta> = {};
|
const topLevelItems: Record<string, ConfigMeta> = {};
|
||||||
for (const item of questionableTagRenderingSchemaRaw) {
|
for (const item of questionableTagRenderingSchemaRaw) {
|
||||||
if (item.path.length === 1) {
|
if (item.path.length === 1) {
|
||||||
|
@ -81,7 +80,6 @@ const missing: string[] = questionableTagRenderingSchemaRaw.filter(schema => sch
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if typeof value === "string"}
|
{#if typeof value === "string"}
|
||||||
|
|
||||||
<div class="flex low-interaction">
|
<div class="flex low-interaction">
|
||||||
<TagRenderingEditable config={configBuiltin} selectedElement={undefined} showQuestionIfUnknown={true} {state}
|
<TagRenderingEditable config={configBuiltin} selectedElement={undefined} showQuestionIfUnknown={true} {state}
|
||||||
{tags} />
|
{tags} />
|
||||||
|
@ -92,12 +90,9 @@ const missing: string[] = questionableTagRenderingSchemaRaw.filter(schema => sch
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<slot name="upper-right" />
|
<slot name="upper-right" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $allowQuestions}
|
{#if $allowQuestions}
|
||||||
<SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]} />
|
<SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]} />
|
||||||
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]} />
|
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]} />
|
||||||
{:else}
|
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
{#each ($mappings ?? []) as mapping, i (mapping)}
|
{#each ($mappings ?? []) as mapping, i (mapping)}
|
||||||
<div class="flex interactive w-full">
|
<div class="flex interactive w-full">
|
||||||
|
|
|
@ -2,13 +2,10 @@
|
||||||
|
|
||||||
|
|
||||||
import NextButton from "./Base/NextButton.svelte";
|
import NextButton from "./Base/NextButton.svelte";
|
||||||
import { UIEventSource } from "../Logic/UIEventSource";
|
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||||
import ValidatedInput from "./InputElement/ValidatedInput.svelte";
|
|
||||||
import EditLayerState from "./Studio/EditLayerState";
|
import EditLayerState from "./Studio/EditLayerState";
|
||||||
import EditLayer from "./Studio/EditLayer.svelte";
|
import EditLayer from "./Studio/EditLayer.svelte";
|
||||||
import Loading from "../assets/svg/Loading.svelte";
|
import Loading from "../assets/svg/Loading.svelte";
|
||||||
import Marker from "./Map/Marker.svelte";
|
|
||||||
import { AllSharedLayers } from "../Customizations/AllSharedLayers";
|
|
||||||
import StudioServer from "./Studio/StudioServer";
|
import StudioServer from "./Studio/StudioServer";
|
||||||
import LoginToggle from "./Base/LoginToggle.svelte";
|
import LoginToggle from "./Base/LoginToggle.svelte";
|
||||||
import { OsmConnection } from "../Logic/Osm/OsmConnection";
|
import { OsmConnection } from "../Logic/Osm/OsmConnection";
|
||||||
|
@ -17,53 +14,54 @@
|
||||||
import layerSchemaRaw from "../../src/assets/schemas/layerconfigmeta.json";
|
import layerSchemaRaw from "../../src/assets/schemas/layerconfigmeta.json";
|
||||||
import If from "./Base/If.svelte";
|
import If from "./Base/If.svelte";
|
||||||
import BackButton from "./Base/BackButton.svelte";
|
import BackButton from "./Base/BackButton.svelte";
|
||||||
|
import ChooseLayerToEdit from "./Studio/ChooseLayerToEdit.svelte";
|
||||||
|
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource";
|
||||||
|
import FloatOver from "./Base/FloatOver.svelte";
|
||||||
|
import Walkthrough from "./Walkthrough/Walkthrough.svelte";
|
||||||
|
import * as intro from "../assets/studio_introduction.json";
|
||||||
|
import { QuestionMarkCircleIcon } from "@babeard/svelte-heroicons/mini";
|
||||||
|
import type { ConfigMeta } from "./Studio/configMeta";
|
||||||
|
|
||||||
export let studioUrl = window.location.hostname === "127.0.0.1" ? "http://127.0.0.1:1235" : "https://studio.mapcomplete.org";
|
export let studioUrl = window.location.hostname === "127.0.0.1" ? "http://127.0.0.1:1235" : "https://studio.mapcomplete.org";
|
||||||
const studio = new StudioServer(studioUrl);
|
|
||||||
let layersWithErr = UIEventSource.FromPromiseWithErr(studio.fetchLayerOverview());
|
let osmConnection = new OsmConnection(new OsmConnection({
|
||||||
let layers = layersWithErr.mapD(l => l.success);
|
oauth_token: QueryParameters.GetQueryParameter(
|
||||||
let state: undefined | "edit_layer" | "new_layer" | "edit_theme" | "new_theme" | "editing_layer" | "loading" = undefined;
|
"oauth_token",
|
||||||
|
undefined,
|
||||||
|
"Used to complete the login"
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
const createdBy = osmConnection.userDetails.data.name;
|
||||||
|
const uid = osmConnection.userDetails.map(ud => ud?.uid);
|
||||||
|
const studio = new StudioServer(studioUrl, uid);
|
||||||
|
|
||||||
|
let layersWithErr = uid.bind(uid => UIEventSource.FromPromiseWithErr(studio.fetchLayerOverview()));
|
||||||
|
let layers: Store<{ owner: number }[]> = layersWithErr.mapD(l => l.success);
|
||||||
|
let selfLayers = layers.mapD(ls => ls.filter(l => l.owner === uid.data), [uid]);
|
||||||
|
let otherLayers = layers.mapD(ls => ls.filter(l => l.owner !== uid.data), [uid]);
|
||||||
|
|
||||||
|
let state: undefined | "edit_layer" | "edit_theme" | "new_theme" | "editing_layer" | "loading" = undefined;
|
||||||
|
|
||||||
let initialLayerConfig: { id: string };
|
let initialLayerConfig: { id: string };
|
||||||
let newLayerId = new UIEventSource<string>("");
|
|
||||||
/**
|
|
||||||
* Also used in the input field as 'feedback', hence not a mappedStore as it must be writable
|
|
||||||
*/
|
|
||||||
let layerIdFeedback = new UIEventSource<string>(undefined);
|
|
||||||
newLayerId.addCallbackD(layerId => {
|
|
||||||
if (layerId === "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (layers.data?.has(layerId)) {
|
|
||||||
layerIdFeedback.setData("This id is already used");
|
|
||||||
}
|
|
||||||
}, [layers]);
|
|
||||||
|
|
||||||
|
|
||||||
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
|
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
|
||||||
|
|
||||||
let editLayerState = new EditLayerState(layerSchema, studio);
|
let editLayerState = new EditLayerState(layerSchema, studio, osmConnection);
|
||||||
let layerId = editLayerState.configuration.map(layerConfig => layerConfig.id);
|
let layerId = editLayerState.configuration.map(layerConfig => layerConfig.id);
|
||||||
|
|
||||||
function fetchIconDescription(layerId): any {
|
let showIntro = UIEventSource.asBoolean(LocalStorageSource.Get("studio-show-intro", "true"));
|
||||||
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
|
|
||||||
|
async function editLayer(event: Event) {
|
||||||
|
const layerId = event.detail;
|
||||||
|
state = "loading";
|
||||||
|
initialLayerConfig = await studio.fetchLayer(layerId);
|
||||||
|
state = "editing_layer";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNewLayer() {
|
async function createNewLayer() {
|
||||||
if (layerIdFeedback.data !== undefined) {
|
|
||||||
console.warn("There is still some feedback - not starting to create a new layer");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state = "loading";
|
state = "loading";
|
||||||
const id = newLayerId.data;
|
initialLayerConfig = {
|
||||||
const createdBy = osmConnection.userDetails.data.name;
|
credits: createdBy,
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
const loaded = await studio.fetchLayer(id);
|
|
||||||
initialLayerConfig = loaded ?? {
|
|
||||||
id, credits: createdBy,
|
|
||||||
minzoom: 15,
|
minzoom: 15,
|
||||||
pointRendering: [
|
pointRendering: [
|
||||||
{
|
{
|
||||||
|
@ -79,19 +77,9 @@
|
||||||
color: "blue"
|
color: "blue"
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
} catch (e) {
|
|
||||||
initialLayerConfig = { id, credits: createdBy };
|
|
||||||
}
|
|
||||||
state = "editing_layer";
|
state = "editing_layer";
|
||||||
}
|
}
|
||||||
|
|
||||||
let osmConnection = new OsmConnection(new OsmConnection({
|
|
||||||
oauth_token: QueryParameters.GetQueryParameter(
|
|
||||||
"oauth_token",
|
|
||||||
undefined,
|
|
||||||
"Used to complete the login"
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -125,13 +113,14 @@
|
||||||
</NextButton>
|
</NextButton>
|
||||||
</div>
|
</div>
|
||||||
{#if state === undefined}
|
{#if state === undefined}
|
||||||
|
<div class="m-4">
|
||||||
<h1>MapComplete Studio</h1>
|
<h1>MapComplete Studio</h1>
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
|
|
||||||
<NextButton on:click={() => state = "edit_layer"}>
|
<NextButton on:click={() => state = "edit_layer"}>
|
||||||
Edit an existing layer
|
Edit an existing layer
|
||||||
</NextButton>
|
</NextButton>
|
||||||
<NextButton on:click={() => state = "new_layer"}>
|
<NextButton on:click={() => createNewLayer()}>
|
||||||
Create a new layer
|
Create a new layer
|
||||||
</NextButton>
|
</NextButton>
|
||||||
<!--
|
<!--
|
||||||
|
@ -142,58 +131,43 @@
|
||||||
Create a new theme
|
Create a new theme
|
||||||
</NextButton>
|
</NextButton>
|
||||||
-->
|
-->
|
||||||
|
<NextButton clss="small" on:click={() => {showIntro.setData(true)} }>
|
||||||
|
<QuestionMarkCircleIcon class="w-6 h-6" />
|
||||||
|
Show the introduction again
|
||||||
|
</NextButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if state === "edit_layer"}
|
{:else if state === "edit_layer"}
|
||||||
|
|
||||||
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio</BackButton>
|
<div class="flex flex-col m-4">
|
||||||
<h3>Choose a layer to edit</h3>
|
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
|
||||||
<div class="flex flex-wrap">
|
</BackButton>
|
||||||
{#each Array.from($layers) as layerId}
|
<h2>Choose a layer to edit</h2>
|
||||||
<NextButton clss="small" on:click={async () => {
|
<ChooseLayerToEdit layerIds={$selfLayers} on:layerSelected={editLayer}>
|
||||||
state = "loading"
|
<h3 slot="title">Your layers</h3>
|
||||||
initialLayerConfig = await studio.fetchLayer(layerId)
|
</ChooseLayerToEdit>
|
||||||
state = "editing_layer"
|
<h3>Official layers</h3>
|
||||||
}}>
|
<ChooseLayerToEdit layerIds={$otherLayers} on:layerSelected={editLayer} />
|
||||||
<div class="w-4 h-4 mr-1">
|
|
||||||
<Marker icons={fetchIconDescription(layerId)} />
|
|
||||||
</div>
|
|
||||||
{layerId}
|
|
||||||
</NextButton>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else if state === "new_layer"}
|
|
||||||
|
|
||||||
<div class="interactive flex m-2 rounded-2xl flex-col p-2">
|
|
||||||
<h3>Enter the ID for the new layer</h3>
|
|
||||||
A good ID is:
|
|
||||||
<ul>
|
|
||||||
<li>a noun</li>
|
|
||||||
<li>singular</li>
|
|
||||||
<li>describes the object</li>
|
|
||||||
<li>in English</li>
|
|
||||||
</ul>
|
|
||||||
<div class="m-2 p-2 w-full">
|
|
||||||
|
|
||||||
<ValidatedInput type="id" value={newLayerId} feedback={layerIdFeedback} on:submit={() => createNewLayer()} />
|
|
||||||
</div>
|
|
||||||
{#if $layerIdFeedback !== undefined}
|
|
||||||
<div class="alert">
|
|
||||||
{$layerIdFeedback}
|
|
||||||
</div>
|
|
||||||
{:else }
|
|
||||||
<NextButton clss="primary" on:click={() => createNewLayer()}>
|
|
||||||
Create layer {$newLayerId}
|
|
||||||
</NextButton>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{:else if state === "loading"}
|
{:else if state === "loading"}
|
||||||
<div class="w-8 h-8">
|
<div class="w-8 h-8">
|
||||||
<Loading />
|
<Loading />
|
||||||
</div>
|
</div>
|
||||||
{:else if state === "editing_layer"}
|
{:else if state === "editing_layer"}
|
||||||
<EditLayer {initialLayerConfig} state={editLayerState} >
|
<EditLayer {initialLayerConfig} state={editLayerState}>
|
||||||
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio</BackButton>
|
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
|
||||||
|
</BackButton>
|
||||||
</EditLayer>
|
</EditLayer>
|
||||||
{/if}
|
{/if}
|
||||||
</LoginToggle>
|
</LoginToggle>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
|
|
||||||
|
{#if $showIntro}
|
||||||
|
<FloatOver>
|
||||||
|
<div class="flex p-4 h-full">
|
||||||
|
<Walkthrough pages={intro.sections} on:done={() => {showIntro.setData(false)}} />
|
||||||
|
</div>
|
||||||
|
</FloatOver>
|
||||||
|
|
||||||
|
{/if}
|
||||||
|
|
Loading…
Reference in a new issue