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)) {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -51,7 +63,9 @@ http.createServer(async (req, res) => {
|
|||
for (let i = 1; i < paths.length; i++) {
|
||||
const p = paths.slice(0, i)
|
||||
const dir = STATIC_PATH + p.join("/")
|
||||
console.log("Checking if", dir, "exists...")
|
||||
if (!fs.existsSync(dir)) {
|
||||
console.log("Creating new directory", dir)
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
}
|
||||
|
@ -61,22 +75,28 @@ http.createServer(async (req, res) => {
|
|||
res.end()
|
||||
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")
|
||||
let userId = url.searchParams.get("userId")
|
||||
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))
|
||||
res.writeHead(200, { "Content-Type": MIME_TYPES.json })
|
||||
res.write(JSON.stringify({ allFiles }))
|
||||
res.end()
|
||||
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)
|
||||
if (file === null) {
|
||||
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)
|
||||
* src.setData(3)
|
||||
* mapped.data // => 6
|
||||
*
|
||||
*/
|
||||
get data(): T {
|
||||
if (!this._callbacksAreRegistered) {
|
||||
|
|
|
@ -666,25 +666,29 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
}
|
||||
|
||||
if (json.freeform) {
|
||||
const c = context.enters("freeform", "render")
|
||||
if (json.render === undefined) {
|
||||
c.err(
|
||||
"This tagRendering allows to set a freeform, but does not define a way to `render` this value"
|
||||
)
|
||||
context
|
||||
.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 {
|
||||
const render = new Translation(<any>json.render)
|
||||
|
||||
for (const ln in render.translations) {
|
||||
if (ln.startsWith("_")) {
|
||||
continue
|
||||
}
|
||||
const txt: string = render.translations[ln]
|
||||
if (txt === "") {
|
||||
c.err(" Rendering for language " + ln + " is empty")
|
||||
context.enter("render").err(" Rendering for language " + ln + " is empty")
|
||||
}
|
||||
if (
|
||||
txt.indexOf("{" + json.freeform.key + "}") >= 0 ||
|
||||
txt.indexOf("&LBRACE" + json.freeform.key + "&RBRACE")
|
||||
txt.indexOf("&LBRACE" + json.freeform.key + "&RBRACE") >= 0
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
@ -721,9 +725,11 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
) {
|
||||
continue
|
||||
}
|
||||
c.err(
|
||||
`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} `
|
||||
)
|
||||
context
|
||||
.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 _isBuiltin: boolean
|
||||
private readonly _doesImageExist: DoesImageExist
|
||||
private _studioValidations: boolean
|
||||
private readonly _studioValidations: boolean
|
||||
|
||||
constructor(
|
||||
path: string,
|
||||
|
@ -816,7 +822,7 @@ export class ValidateLayer extends Conversion<
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -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."
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
// 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) {
|
||||
|
|
|
@ -15,11 +15,18 @@ import { Translatable } from "./Translatable"
|
|||
*/
|
||||
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?
|
||||
*
|
||||
* 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
|
||||
|
||||
|
|
|
@ -26,8 +26,7 @@
|
|||
}
|
||||
const apiState = state.osmConnection.apiIsOnline
|
||||
</script>
|
||||
<slot />
|
||||
<!--
|
||||
|
||||
{#if $badge}
|
||||
{#if !ignoreLoading && $loadingStatus === "loading"}
|
||||
<slot name="loading">
|
||||
|
@ -43,4 +42,4 @@
|
|||
{:else if $loadingStatus === "not-attempted"}
|
||||
<slot name="not-logged-in" />
|
||||
{/if}
|
||||
{/if} -->
|
||||
{/if}
|
||||
|
|
|
@ -22,7 +22,7 @@ $: documentation = TagUtils.modeDocumentation[mode];
|
|||
</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}
|
||||
<div class="border border-dashed border-black p-2 m-2">
|
||||
<b>{documentation.name}</b>
|
||||
|
|
|
@ -19,4 +19,4 @@ let tag: UIEventSource<string | TagConfigJson> = value
|
|||
</script>
|
||||
|
||||
|
||||
<FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} />
|
||||
<FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} on:submit/>
|
||||
|
|
|
@ -5,20 +5,14 @@
|
|||
import { createEventDispatcher, onDestroy } from "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[] = []
|
||||
|
||||
let prefix = args[0]
|
||||
let postfix = args[1]
|
||||
let prefix = args[0] ?? ""
|
||||
let postfix = args[1] ?? ""
|
||||
|
||||
let translations: UIEventSource<Record<string, string>> = value.sync((s) => {
|
||||
try {
|
||||
return JSON.parse(s);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}, [], v => JSON.stringify(v));
|
||||
let translations: UIEventSource<Record<string, string>> = value
|
||||
|
||||
const allLanguages: string[] = LanguageUtils.usedLanguagesSorted;
|
||||
let currentLang = new UIEventSource("en");
|
||||
|
@ -28,6 +22,9 @@
|
|||
function update() {
|
||||
const v = currentVal.data;
|
||||
const l = currentLang.data;
|
||||
if(translations.data === "" || translations.data === undefined){
|
||||
translations.data = {}
|
||||
}
|
||||
if (translations.data[l] === v) {
|
||||
return;
|
||||
}
|
||||
|
@ -37,6 +34,9 @@
|
|||
|
||||
onDestroy(currentLang.addCallbackAndRunD(currentLang => {
|
||||
console.log("Applying current lang:", currentLang);
|
||||
if(!translations.data){
|
||||
translations.data = {}
|
||||
}
|
||||
translations.data[currentLang] = translations.data[currentLang] ?? "";
|
||||
currentVal.setData(translations.data[currentLang]);
|
||||
}));
|
||||
|
|
|
@ -27,14 +27,13 @@
|
|||
|
||||
let properties = { feature, args: args ?? [] };
|
||||
let dispatch = createEventDispatcher<{
|
||||
selected,
|
||||
submit
|
||||
selected
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
{#if type === "translation" }
|
||||
<TranslationInput {value} on:submit={() => dispatch("submit")} {args} />
|
||||
<TranslationInput {value} on:submit {args} />
|
||||
{:else if type === "direction"}
|
||||
<DirectionInput {value} mapProperties={InputHelpers.constructMapProperties(properties)} />
|
||||
{:else if type === "date"}
|
||||
|
@ -44,9 +43,9 @@
|
|||
{:else if type === "image"}
|
||||
<ImageHelper { value } />
|
||||
{:else if type === "tag"}
|
||||
<TagInput { value } />
|
||||
<TagInput { value } on:submit />
|
||||
{:else if type === "simple_tag"}
|
||||
<SimpleTagInput { value } {args} />
|
||||
<SimpleTagInput { value } {args} on:submit />
|
||||
{:else if type === "opening_hours"}
|
||||
<OpeningHoursInput { value } />
|
||||
{:else if type === "wikidata"}
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
* Dispatches the submit, but only if the value is valid
|
||||
*/
|
||||
function sendSubmit(){
|
||||
if(feedback.data){
|
||||
if(feedback?.data){
|
||||
console.log("Not sending a submit as there is feedback")
|
||||
}
|
||||
dispatch("submit")
|
||||
|
|
|
@ -259,7 +259,6 @@
|
|||
value={freeformInput}
|
||||
on:selected={() => (selectedMapping = config.mappings?.length)}
|
||||
on:submit={onSave}
|
||||
submit={onSave}
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
|
|
|
@ -1336,6 +1336,7 @@ export default class SpecialVisualizations {
|
|||
const tr = typeof v === "string" ? JSON.parse(v) : v
|
||||
return new Translation(tr).SetClass("font-bold")
|
||||
} catch (e) {
|
||||
console.error("Cannot create a translation for", v, "due to", e)
|
||||
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 ErrorIndicatorForRegion from "./ErrorIndicatorForRegion.svelte";
|
||||
import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import SchemaBasedInput from "./SchemaBasedInput.svelte";
|
||||
|
||||
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
|
||||
|
||||
|
@ -25,7 +26,6 @@
|
|||
* Blacklist of regions for the general area 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 perRegion: Record<string, ConfigMeta[]> = {};
|
||||
|
@ -33,12 +33,7 @@
|
|||
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 wl = window.location;
|
||||
const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout=";
|
||||
|
@ -46,80 +41,122 @@
|
|||
function firstPathsFor(...regionNames: string[]): Set<string> {
|
||||
const pathNames = new Set<string>();
|
||||
for (const regionName of regionNames) {
|
||||
const region: ConfigMeta[] = perRegion[regionName]
|
||||
const region: ConfigMeta[] = perRegion[regionName];
|
||||
for (const configMeta of region) {
|
||||
pathNames.add(configMeta.path[0])
|
||||
pathNames.add(configMeta.path[0]);
|
||||
}
|
||||
}
|
||||
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>
|
||||
|
||||
<div class="w-full flex justify-between">
|
||||
<slot />
|
||||
<h3>Editing layer {$title}</h3>
|
||||
{#if $hasErrors > 0}
|
||||
<div class="alert">{$hasErrors} errors detected</div>
|
||||
{:else}
|
||||
<a class="primary button" href={baseUrl+state.server.layerUrl(title.data)} target="_blank" rel="noopener">
|
||||
Try it out
|
||||
<ChevronRightIcon class= "h-6 w-6 shrink-0"/>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="m4">
|
||||
<TabbedGroup>
|
||||
<div slot="title0" class="flex">General properties
|
||||
<ErrorIndicatorForRegion firstPaths={firstPathsFor(...baselayerRegions)} {state} />
|
||||
</div>
|
||||
<div class="flex flex-col" slot="content0">
|
||||
{#each baselayerRegions as region}
|
||||
<Region {state} configs={perRegion[region]} title={region} />
|
||||
{/each}
|
||||
</div>
|
||||
<div slot="title1" class="flex">Information panel (questions and answers)
|
||||
<ErrorIndicatorForRegion firstPaths={firstPathsFor("title","tagrenderings","editing")} {state} /></div>
|
||||
<div slot="content1">
|
||||
<Region configs={perRegion["title"]} {state} title="Popup title" />
|
||||
<Region configs={perRegion["tagrenderings"]} {state} title="Popup contents" />
|
||||
<Region configs={perRegion["editing"]} {state} title="Other editing elements" />
|
||||
</div>
|
||||
{#if $currentlyMissing.length > 0}
|
||||
|
||||
<div slot="title2" class="flex">Rendering on the map
|
||||
<ErrorIndicatorForRegion firstPaths={firstPathsFor("linerendering","pointrendering")} {state} /></div>
|
||||
<div slot="content2">
|
||||
<Region configs={perRegion["linerendering"]} {state} />
|
||||
<Region configs={perRegion["pointrendering"]} {state} />
|
||||
</div>
|
||||
{#each requiredFields as required}
|
||||
<SchemaBasedInput {state}
|
||||
schema={configForRequiredField(required)}
|
||||
path={[required]} />
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="w-full flex justify-between my-2">
|
||||
<slot />
|
||||
<h3>Editing layer {$title}</h3>
|
||||
{#if $hasErrors > 0}
|
||||
<div class="alert">{$hasErrors} errors detected</div>
|
||||
{:else}
|
||||
<a class="primary button" href={baseUrl+state.server.layerUrl(title.data)} target="_blank" rel="noopener">
|
||||
Try it out
|
||||
<ChevronRightIcon class="h-6 w-6 shrink-0" />
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="m4">
|
||||
<TabbedGroup>
|
||||
<div slot="title0" class="flex">General properties
|
||||
<ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} />
|
||||
</div>
|
||||
<div class="flex flex-col" slot="content0">
|
||||
<Region {state} configs={perRegion["Basic"]} />
|
||||
|
||||
<div slot="title3" class="flex">Advanced functionality
|
||||
<ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced","expert")} {state} /></div>
|
||||
<div slot="content3">
|
||||
<Region configs={perRegion["advanced"]} {state} />
|
||||
<Region configs={perRegion["expert"]} {state} />
|
||||
</div>
|
||||
<div slot="title4">Configuration file</div>
|
||||
<div slot="content4">
|
||||
<div>
|
||||
Below, you'll find the raw configuration file in `.json`-format.
|
||||
This is mostly for debugging purposes
|
||||
</div>
|
||||
<div class="literal-code">
|
||||
{JSON.stringify($configuration, null, " ")}
|
||||
|
||||
|
||||
<div slot="title1" class="flex">Information panel (questions and answers)
|
||||
<ErrorIndicatorForRegion firstPaths={firstPathsFor("title","tagrenderings","editing")} {state} />
|
||||
</div>
|
||||
{#each $messages as message}
|
||||
<li>
|
||||
{message.level}
|
||||
<span class="literal-code">{message.context.path.join(".")}</span>
|
||||
{message.message}
|
||||
<span class="literal-code">
|
||||
<div slot="content1">
|
||||
<Region configs={perRegion["title"]} {state} title="Popup title" />
|
||||
<Region configs={perRegion["tagrenderings"]} {state} title="Popup contents" />
|
||||
<Region configs={perRegion["editing"]} {state} title="Other editing elements" />
|
||||
</div>
|
||||
|
||||
<div slot="title2">
|
||||
<ErrorIndicatorForRegion firstPaths={firstPathsFor("presets")} {state} />
|
||||
Creating a new point
|
||||
</div>
|
||||
|
||||
<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["pointrendering"]} {state} />
|
||||
</div>
|
||||
|
||||
<div slot="title4" class="flex">Advanced functionality
|
||||
<ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced","expert")} {state} />
|
||||
</div>
|
||||
<div slot="content4">
|
||||
<Region configs={perRegion["advanced"]} {state} />
|
||||
<Region configs={perRegion["expert"]} {state} />
|
||||
</div>
|
||||
<div slot="title5">Configuration file</div>
|
||||
<div slot="content5">
|
||||
<div>
|
||||
Below, you'll find the raw configuration file in `.json`-format.
|
||||
This is mosSendertly for debugging purposes
|
||||
</div>
|
||||
<div class="literal-code">
|
||||
{JSON.stringify($configuration, null, " ")}
|
||||
</div>
|
||||
{#each $messages as message}
|
||||
<li>
|
||||
{message.level}
|
||||
<span class="literal-code">{message.context.path.join(".")}</span>
|
||||
{message.message}
|
||||
<span class="literal-code">
|
||||
{message.context.operation.join(".")}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
</div>
|
||||
</TabbedGroup>
|
||||
</li>
|
||||
{/each}
|
||||
</div>
|
||||
</TabbedGroup>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { ConfigMeta } from "./configMeta"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import { QueryParameters } from "../../Logic/Web/QueryParameters"
|
||||
import {
|
||||
ConversionContext,
|
||||
ConversionMessage,
|
||||
|
@ -16,25 +14,29 @@ import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Jso
|
|||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import StudioServer from "./StudioServer"
|
||||
import { Utils } from "../../Utils"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
|
||||
/**
|
||||
* Sends changes back to the server
|
||||
*/
|
||||
export class LayerStateSender {
|
||||
constructor(layerState: EditLayerState) {
|
||||
layerState.configuration.addCallback(async (config) => {
|
||||
const id = config.id
|
||||
if (id === undefined) {
|
||||
console.warn("No id found in layer, not updating")
|
||||
return
|
||||
}
|
||||
await layerState.server.updateLayer(<LayerConfigJson>config)
|
||||
})
|
||||
const layerId = layerState.configuration.map((config) => config.id)
|
||||
layerState.configuration
|
||||
.mapD((config) => JSON.stringify(config, null, " "))
|
||||
.stabilized(100)
|
||||
.addCallbackD(async (config) => {
|
||||
const id = layerId.data
|
||||
if (id === undefined) {
|
||||
console.warn("No id found in layer, not updating")
|
||||
return
|
||||
}
|
||||
await layerState.server.updateLayer(id, config)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default class EditLayerState {
|
||||
public readonly osmConnection: OsmConnection
|
||||
public readonly schema: ConfigMeta[]
|
||||
|
||||
public readonly featureSwitches: { featureSwitchIsDebugging: UIEventSource<boolean> }
|
||||
|
@ -44,17 +46,14 @@ export default class EditLayerState {
|
|||
>({})
|
||||
public readonly messages: Store<ConversionMessage[]>
|
||||
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.server = server
|
||||
this.osmConnection = new OsmConnection({
|
||||
oauth_token: QueryParameters.GetQueryParameter(
|
||||
"oauth_token",
|
||||
undefined,
|
||||
"Used to complete the login"
|
||||
),
|
||||
})
|
||||
this.osmConnection = osmConnection
|
||||
this.featureSwitches = {
|
||||
featureSwitchIsDebugging: new UIEventSource<boolean>(true),
|
||||
}
|
||||
|
@ -118,7 +117,6 @@ export default class EditLayerState {
|
|||
return entry
|
||||
}
|
||||
|
||||
private readonly _stores = new Map<string, UIEventSource<any>>()
|
||||
public getStoreFor<T>(path: ReadonlyArray<string | number>): UIEventSource<T | undefined> {
|
||||
const key = path.join(".")
|
||||
|
||||
|
@ -139,7 +137,9 @@ export default class EditLayerState {
|
|||
value: Store<any>,
|
||||
noInitialSync: boolean = false
|
||||
): () => void {
|
||||
const unsync = value.addCallback((v) => this.setValueAt(path, v))
|
||||
const unsync = value.addCallback((v) => {
|
||||
this.setValueAt(path, v)
|
||||
})
|
||||
if (!noInitialSync) {
|
||||
this.setValueAt(path, value.data)
|
||||
}
|
||||
|
@ -180,6 +180,7 @@ export default class EditLayerState {
|
|||
|
||||
public setValueAt(path: ReadonlyArray<string | number>, v: any) {
|
||||
let entry = this.configuration.data
|
||||
console.log("Setting value at", path, v)
|
||||
const isUndefined =
|
||||
v === undefined ||
|
||||
v === null ||
|
||||
|
@ -197,15 +198,35 @@ export default class EditLayerState {
|
|||
}
|
||||
entry = entry[breadcrumb]
|
||||
}
|
||||
|
||||
const lastBreadcrumb = path.at(-1)
|
||||
if (isUndefined) {
|
||||
if (entry && entry[lastBreadcrumb]) {
|
||||
console.log("Deleting", lastBreadcrumb, "of", path.join("."))
|
||||
delete entry[lastBreadcrumb]
|
||||
this.configuration.ping()
|
||||
}
|
||||
} else {
|
||||
} else if (entry[lastBreadcrumb] !== v) {
|
||||
console.log("Assigning and pinging at", path)
|
||||
entry[lastBreadcrumb] = v
|
||||
this.configuration.ping()
|
||||
}
|
||||
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}
|
||||
<div class="border border-black">
|
||||
{#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"
|
||||
on:click={() => {del(value)}}>
|
||||
<TrashIcon class="w-4 h-4" />
|
||||
|
|
|
@ -21,12 +21,12 @@
|
|||
const isTranslation = schema.hints.typehint === "translation" || schema.hints.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema);
|
||||
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"){
|
||||
rendervalue = undefined
|
||||
}
|
||||
if(schema.hints.typehint === "tag") {
|
||||
if(schema.hints.typehint === "tag" || schema.hints.typehint === "simple_tag") {
|
||||
rendervalue = "{tags()}"
|
||||
}
|
||||
|
||||
|
@ -61,12 +61,12 @@
|
|||
if (schema.hints.default) {
|
||||
configJson.mappings = [{
|
||||
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) {
|
||||
configJson.mappings = [{
|
||||
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 err: string = undefined;
|
||||
let messages = state.messages.mapD(msgs => 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;
|
||||
}));
|
||||
let messages = state.messagesFor(path)
|
||||
try {
|
||||
config = new TagRenderingConfig(configJson, "config based on " + schema.path.join("."));
|
||||
} catch (e) {
|
||||
|
@ -130,7 +122,7 @@
|
|||
onDestroy(state.register(path, tags.map(tgs => {
|
||||
const v = tgs["value"];
|
||||
if (typeof v !== "string") {
|
||||
return v;
|
||||
return { ... v };
|
||||
}
|
||||
if (schema.type === "boolan") {
|
||||
return v === "true" || v === "yes" || v === "1";
|
||||
|
@ -140,7 +132,6 @@
|
|||
return true;
|
||||
}
|
||||
if (v === "false" || v === "no" || v === "0") {
|
||||
console.log("Setting false...");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -173,7 +164,7 @@
|
|||
{/each}
|
||||
{/if}
|
||||
{#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}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
{#if schema.hints.typehint === "tagrendering[]"}
|
||||
<!-- We cheat a bit here by matching this 'magical' type... -->
|
||||
<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}/>
|
||||
{:else if schema.type === "array"}
|
||||
<SchemaBasedArray {path} {state} {schema} />
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
import type { JsonSchemaType } from "./jsonSchema";
|
||||
// @ts-ignore
|
||||
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.
|
||||
|
@ -30,7 +29,7 @@
|
|||
types.splice(hasBooleanOption);
|
||||
}
|
||||
|
||||
|
||||
|
||||
let lastIsString = false;
|
||||
{
|
||||
const types: string | string[] = Array.isArray(schema.type) ? schema.type[schema.type.length - 1].type : [];
|
||||
|
@ -42,7 +41,7 @@
|
|||
}
|
||||
const configJson: QuestionableTagRenderingConfigJson = {
|
||||
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),
|
||||
mappings: types.map(opt => opt.trim()).filter(opt => opt.length > 0).map((opt, i) => ({
|
||||
if: "chosen_type_index=" + i,
|
||||
|
@ -127,14 +126,14 @@
|
|||
possibleTypes.sort((a, b) => b.optionalMatches - a.optionalMatches);
|
||||
possibleTypes.sort((a, b) => b.matchingPropertiesCount - a.matchingPropertiesCount);
|
||||
if (possibleTypes.length > 0) {
|
||||
chosenOption = possibleTypes[0].index
|
||||
tags.setData({ chosen_type_index: "" + chosenOption});
|
||||
|
||||
chosenOption = possibleTypes[0].index;
|
||||
tags.setData({ chosen_type_index: "" + chosenOption });
|
||||
|
||||
}
|
||||
} else if (defaultOption !== undefined) {
|
||||
tags.setData({ chosen_type_index: "" + defaultOption });
|
||||
}else{
|
||||
chosenOption = defaultOption
|
||||
} else {
|
||||
chosenOption = defaultOption;
|
||||
}
|
||||
|
||||
if (hasBooleanOption >= 0 || lastIsString) {
|
||||
|
@ -154,7 +153,7 @@
|
|||
let subSchemas: ConfigMeta[] = [];
|
||||
|
||||
let subpath = path;
|
||||
const store = state.getStoreFor(path)
|
||||
const store = state.getStoreFor(path);
|
||||
onDestroy(tags.addCallbackAndRun(tags => {
|
||||
if (tags["value"] !== undefined && tags["value"] !== "") {
|
||||
chosenOption = undefined;
|
||||
|
@ -170,7 +169,7 @@
|
|||
for (const key of type?.required ?? []) {
|
||||
o[key] ??= {};
|
||||
}
|
||||
store.setData(o)
|
||||
store.setData(o);
|
||||
}
|
||||
if (!type) {
|
||||
return;
|
||||
|
@ -191,6 +190,7 @@
|
|||
subSchemas.push(...(state.getSchema([...cleanPath, crumble])));
|
||||
}
|
||||
}));
|
||||
let messages = state.messagesFor(path);
|
||||
|
||||
|
||||
</script>
|
||||
|
@ -209,5 +209,9 @@
|
|||
<SchemaBasedInput {state} schema={subschema}
|
||||
path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput>
|
||||
{/each}
|
||||
{:else if $messages.length > 0}
|
||||
{#each $messages as msg}
|
||||
<div class="alert">{msg.message}</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
export let path: (string | number)[] = [];
|
||||
export let schema: ConfigMeta;
|
||||
|
||||
let value = new UIEventSource<string>("{}");
|
||||
let value = new UIEventSource<string>({});
|
||||
console.log("Registering translation to path", path)
|
||||
state.register(path, value.mapD(v => JSON.parse(value.data )));
|
||||
</script>
|
||||
|
|
|
@ -1,23 +1,51 @@
|
|||
import { Utils } from "../../Utils"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
|
||||
export default class StudioServer {
|
||||
private readonly url: string
|
||||
private readonly _userId: Store<number>
|
||||
|
||||
constructor(url: string) {
|
||||
constructor(url: string, userId: Store<number>) {
|
||||
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[] }>(
|
||||
await Utils.downloadJson(this.url + "/overview")
|
||||
await Utils.downloadJson(this.url + "/overview" + uidQueryParam)
|
||||
)
|
||||
const layers = allFiles
|
||||
.filter((f) => f.startsWith("layers/"))
|
||||
.map((l) => l.substring(l.lastIndexOf("/") + 1, l.length - ".json".length))
|
||||
.filter((layerId) => Constants.priviliged_layers.indexOf(<any>layerId) < 0)
|
||||
return new Set<string>(layers)
|
||||
const layerOverview: {
|
||||
id: string
|
||||
owner: number | undefined
|
||||
}[] = []
|
||||
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> {
|
||||
|
@ -28,8 +56,7 @@ export default class StudioServer {
|
|||
}
|
||||
}
|
||||
|
||||
async updateLayer(config: LayerConfigJson) {
|
||||
const id = config.id
|
||||
async updateLayer(id: string, config: string) {
|
||||
if (id === undefined || id === "") {
|
||||
return
|
||||
}
|
||||
|
@ -38,11 +65,13 @@ export default class StudioServer {
|
|||
headers: {
|
||||
"Content-Type": "application/json;charset=utf-8",
|
||||
},
|
||||
body: JSON.stringify(config, null, " "),
|
||||
body: config,
|
||||
})
|
||||
}
|
||||
|
||||
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">
|
||||
{#each $basicTags as basicTag (basicTag)}
|
||||
<div class="flex">
|
||||
<BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} />
|
||||
<BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} on:submit />
|
||||
{#if $basicTags.length + $expressions.length > 1}
|
||||
<button class="border border-black rounded-full w-fit h-fit p-0"
|
||||
on:click={() => removeTag(basicTag)}>
|
||||
|
|
|
@ -111,7 +111,7 @@
|
|||
<div class="flex h-fit ">
|
||||
|
||||
<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)}>
|
||||
{#each modes as option}
|
||||
<option value={option}>
|
||||
|
@ -120,7 +120,7 @@
|
|||
{/each}
|
||||
</select>
|
||||
<ValidatedInput feedback={feedbackValue} placeholder="The value of the tag" type="string"
|
||||
value={valueValue}></ValidatedInput>
|
||||
value={valueValue} on:submit></ValidatedInput>
|
||||
</div>
|
||||
|
||||
{#if $feedbackKey}
|
||||
|
|
|
@ -13,7 +13,7 @@ export let silent: boolean
|
|||
</script>
|
||||
|
||||
<div class="m-2">
|
||||
<TagExpression {silent} {overpassSupportNeeded} {tag} {uploadableOnly}>
|
||||
<TagExpression {silent} {overpassSupportNeeded} {tag} {uploadableOnly} on:submit>
|
||||
<slot name="delete" slot="delete"/>
|
||||
</TagExpression>
|
||||
</div>
|
||||
|
|
|
@ -58,7 +58,6 @@ tags.addCallbackAndRunD(tgs => {
|
|||
|
||||
let mappings: UIEventSource<MappingConfigJson[]> = state.getStoreFor([...path, "mappings"]);
|
||||
|
||||
$: console.log("Allow questions:", $allowQuestions)
|
||||
const topLevelItems: Record<string, ConfigMeta> = {};
|
||||
for (const item of questionableTagRenderingSchemaRaw) {
|
||||
if (item.path.length === 1) {
|
||||
|
@ -81,7 +80,6 @@ const missing: string[] = questionableTagRenderingSchemaRaw.filter(schema => sch
|
|||
</script>
|
||||
|
||||
{#if typeof value === "string"}
|
||||
|
||||
<div class="flex low-interaction">
|
||||
<TagRenderingEditable config={configBuiltin} selectedElement={undefined} showQuestionIfUnknown={true} {state}
|
||||
{tags} />
|
||||
|
@ -92,12 +90,9 @@ const missing: string[] = questionableTagRenderingSchemaRaw.filter(schema => sch
|
|||
<div class="flex justify-end">
|
||||
<slot name="upper-right" />
|
||||
</div>
|
||||
|
||||
{#if $allowQuestions}
|
||||
<SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]} />
|
||||
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]} />
|
||||
{:else}
|
||||
|
||||
{/if}
|
||||
{#each ($mappings ?? []) as mapping, i (mapping)}
|
||||
<div class="flex interactive w-full">
|
||||
|
|
|
@ -2,13 +2,10 @@
|
|||
|
||||
|
||||
import NextButton from "./Base/NextButton.svelte";
|
||||
import { UIEventSource } from "../Logic/UIEventSource";
|
||||
import ValidatedInput from "./InputElement/ValidatedInput.svelte";
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||
import EditLayerState from "./Studio/EditLayerState";
|
||||
import EditLayer from "./Studio/EditLayer.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 LoginToggle from "./Base/LoginToggle.svelte";
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection";
|
||||
|
@ -17,73 +14,15 @@
|
|||
import layerSchemaRaw from "../../src/assets/schemas/layerconfigmeta.json";
|
||||
import If from "./Base/If.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";
|
||||
const studio = new StudioServer(studioUrl);
|
||||
let layersWithErr = UIEventSource.FromPromiseWithErr(studio.fetchLayerOverview());
|
||||
let layers = layersWithErr.mapD(l => l.success);
|
||||
let state: undefined | "edit_layer" | "new_layer" | "edit_theme" | "new_theme" | "editing_layer" | "loading" = undefined;
|
||||
|
||||
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;
|
||||
|
||||
let editLayerState = new EditLayerState(layerSchema, studio);
|
||||
let layerId = editLayerState.configuration.map(layerConfig => layerConfig.id);
|
||||
|
||||
function fetchIconDescription(layerId): any {
|
||||
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
|
||||
}
|
||||
|
||||
async function createNewLayer() {
|
||||
if (layerIdFeedback.data !== undefined) {
|
||||
console.warn("There is still some feedback - not starting to create a new layer");
|
||||
return;
|
||||
}
|
||||
state = "loading";
|
||||
const id = newLayerId.data;
|
||||
const createdBy = osmConnection.userDetails.data.name;
|
||||
|
||||
|
||||
try {
|
||||
|
||||
const loaded = await studio.fetchLayer(id);
|
||||
initialLayerConfig = loaded ?? {
|
||||
id, credits: createdBy,
|
||||
minzoom: 15,
|
||||
pointRendering: [
|
||||
{
|
||||
location: ["point", "centroid"],
|
||||
marker: [{
|
||||
icon: "circle",
|
||||
color: "white"
|
||||
}]
|
||||
}
|
||||
],
|
||||
lineRendering: [{
|
||||
width: 1,
|
||||
color: "blue"
|
||||
}]
|
||||
};
|
||||
} catch (e) {
|
||||
initialLayerConfig = { id, credits: createdBy };
|
||||
}
|
||||
state = "editing_layer";
|
||||
}
|
||||
|
||||
let osmConnection = new OsmConnection(new OsmConnection({
|
||||
oauth_token: QueryParameters.GetQueryParameter(
|
||||
|
@ -92,6 +31,55 @@
|
|||
"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 };
|
||||
|
||||
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
|
||||
|
||||
let editLayerState = new EditLayerState(layerSchema, studio, osmConnection);
|
||||
let layerId = editLayerState.configuration.map(layerConfig => layerConfig.id);
|
||||
|
||||
let showIntro = UIEventSource.asBoolean(LocalStorageSource.Get("studio-show-intro", "true"));
|
||||
|
||||
async function editLayer(event: Event) {
|
||||
const layerId = event.detail;
|
||||
state = "loading";
|
||||
initialLayerConfig = await studio.fetchLayer(layerId);
|
||||
state = "editing_layer";
|
||||
}
|
||||
|
||||
async function createNewLayer() {
|
||||
state = "loading";
|
||||
initialLayerConfig = {
|
||||
credits: createdBy,
|
||||
minzoom: 15,
|
||||
pointRendering: [
|
||||
{
|
||||
location: ["point", "centroid"],
|
||||
marker: [{
|
||||
icon: "circle",
|
||||
color: "white"
|
||||
}]
|
||||
}
|
||||
],
|
||||
lineRendering: [{
|
||||
width: 1,
|
||||
color: "blue"
|
||||
}]
|
||||
};
|
||||
state = "editing_layer";
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
@ -125,75 +113,61 @@
|
|||
</NextButton>
|
||||
</div>
|
||||
{#if state === undefined}
|
||||
<h1>MapComplete Studio</h1>
|
||||
<div class="w-full flex flex-col">
|
||||
<div class="m-4">
|
||||
<h1>MapComplete Studio</h1>
|
||||
<div class="w-full flex flex-col">
|
||||
|
||||
<NextButton on:click={() => state = "edit_layer"}>
|
||||
Edit an existing layer
|
||||
</NextButton>
|
||||
<NextButton on:click={() => state = "new_layer"}>
|
||||
Create a new layer
|
||||
</NextButton>
|
||||
<!--
|
||||
<NextButton on:click={() => state = "edit_theme"}>
|
||||
Edit a theme
|
||||
</NextButton>
|
||||
<NextButton on:click={() => state = "new_theme"}>
|
||||
Create a new theme
|
||||
</NextButton>
|
||||
-->
|
||||
<NextButton on:click={() => state = "edit_layer"}>
|
||||
Edit an existing layer
|
||||
</NextButton>
|
||||
<NextButton on:click={() => createNewLayer()}>
|
||||
Create a new layer
|
||||
</NextButton>
|
||||
<!--
|
||||
<NextButton on:click={() => state = "edit_theme"}>
|
||||
Edit a theme
|
||||
</NextButton>
|
||||
<NextButton on:click={() => state = "new_theme"}>
|
||||
Create a new theme
|
||||
</NextButton>
|
||||
-->
|
||||
<NextButton clss="small" on:click={() => {showIntro.setData(true)} }>
|
||||
<QuestionMarkCircleIcon class="w-6 h-6" />
|
||||
Show the introduction again
|
||||
</NextButton>
|
||||
</div>
|
||||
</div>
|
||||
{:else if state === "edit_layer"}
|
||||
|
||||
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio</BackButton>
|
||||
<h3>Choose a layer to edit</h3>
|
||||
<div class="flex flex-wrap">
|
||||
{#each Array.from($layers) as layerId}
|
||||
<NextButton clss="small" on:click={async () => {
|
||||
state = "loading"
|
||||
initialLayerConfig = await studio.fetchLayer(layerId)
|
||||
state = "editing_layer"
|
||||
}}>
|
||||
<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 class="flex flex-col m-4">
|
||||
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
|
||||
</BackButton>
|
||||
<h2>Choose a layer to edit</h2>
|
||||
<ChooseLayerToEdit layerIds={$selfLayers} on:layerSelected={editLayer}>
|
||||
<h3 slot="title">Your layers</h3>
|
||||
</ChooseLayerToEdit>
|
||||
<h3>Official layers</h3>
|
||||
<ChooseLayerToEdit layerIds={$otherLayers} on:layerSelected={editLayer} />
|
||||
</div>
|
||||
{:else if state === "loading"}
|
||||
<div class="w-8 h-8">
|
||||
<Loading />
|
||||
</div>
|
||||
{:else if state === "editing_layer"}
|
||||
<EditLayer {initialLayerConfig} state={editLayerState} >
|
||||
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio</BackButton>
|
||||
<EditLayer {initialLayerConfig} state={editLayerState}>
|
||||
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
|
||||
</BackButton>
|
||||
</EditLayer>
|
||||
{/if}
|
||||
</LoginToggle>
|
||||
</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