Studio: more finetuning, first version working

This commit is contained in:
Pieter Vander Vennet 2023-10-13 18:46:56 +02:00
parent c4d4a57a08
commit ac1e7c7f06
39 changed files with 2971 additions and 7187 deletions

View file

@ -141,7 +141,7 @@
*/
const kv = selectedTags.asChange(tags.data);
for (const { k, v } of kv) {
if (v === undefined) {
if (v === undefined || v === "") {
delete tags.data[k];
} else {
tags.data[k] = v;

View file

@ -1317,6 +1317,7 @@ export default class SpecialVisualizations {
{
funcName: "translated",
docs: "If the given key can be interpreted as a JSON, only show the key containing the current language (or 'en'). This specialRendering is meant to be used by MapComplete studio and is not useful in map themes",
needsUrls: [],
args: [
{
name: "key",

View file

@ -1,6 +1,6 @@
<script lang="ts">
import EditLayerState, { LayerStateSender } from "./EditLayerState";
import { LayerStateSender } from "./EditLayerState";
import layerSchemaRaw from "../../assets/schemas/layerconfigmeta.json";
import Region from "./Region.svelte";
import TabbedGroup from "../Base/TabbedGroup.svelte";
@ -10,12 +10,13 @@
import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson";
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
let state = new EditLayerState(layerSchema);
export let state;
const messages = state.messages;
export let initialLayerConfig: Partial<LayerConfigJson> = {};
state.configuration.setData(initialLayerConfig);
const configuration = state.configuration;
new LayerStateSender("http://localhost:1235", state);
new LayerStateSender(state);
/**
* Blacklist of regions for the general area tab
* These are regions which are handled by a different tab
@ -76,6 +77,7 @@
</div>
{#each $messages as message}
<li>
{message.level}
<span class="literal-code">{message.context.path.join(".")}</span>
{message.message}
</li>

View file

@ -13,26 +13,21 @@ import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer"
import { ValidateLayer } from "../../Models/ThemeConfig/Conversion/Validation"
import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import StudioServer from "./StudioServer"
/**
* Sends changes back to the server
*/
export class LayerStateSender {
constructor(serverLocation: string, layerState: EditLayerState) {
constructor(layerState: EditLayerState) {
layerState.configuration.addCallback(async (config) => {
// console.log("Current config is", Utils.Clone(config))
const id = config.id
if (id === undefined) {
console.log("No id found in layer, not updating")
console.warn("No id found in layer, not updating")
return
}
const fresponse = await fetch(`${serverLocation}/layers/${id}/${id}.json`, {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify(config, null, " "),
})
await layerState.server.updateLayer(<LayerConfigJson>config)
})
}
}
@ -47,9 +42,11 @@ export default class EditLayerState {
Partial<LayerConfigJson>
>({})
public readonly messages: Store<ConversionMessage[]>
public readonly server: StudioServer
constructor(schema: ConfigMeta[]) {
constructor(schema: ConfigMeta[], server: StudioServer) {
this.schema = schema
this.server = server
this.osmConnection = new OsmConnection({
oauth_token: QueryParameters.GetQueryParameter(
"oauth_token",
@ -73,18 +70,36 @@ export default class EditLayerState {
sharedLayers: layers,
}
}
this.messages = this.configuration.map((config) => {
this.messages = this.configuration.mapD((config) => {
const context = ConversionContext.construct([], ["prepare"])
for (let i = 0; i < (config.tagRenderings ?? []).length; i++) {
const tr = config.tagRenderings[i]
if (typeof tr === "string") {
continue
}
if (!tr["id"] && !tr["override"]) {
const qtr = <QuestionableTagRenderingConfigJson>tr
let id = "" + i
if (qtr?.freeform?.key) {
id = qtr?.freeform?.key
} else if (qtr.mappings?.[0]?.if) {
id =
qtr.freeform?.key ??
TagUtils.Tag(qtr.mappings[0].if).usedKeys()?.[0] ??
"" + i
}
qtr["id"] = id
}
}
const prepare = new Pipe(
new PrepareLayer(state),
new ValidateLayer("dynamic", false, undefined)
)
prepare.convert(<LayerConfigJson>config, context)
console.log(context.messages)
return context.messages
})
console.log("Configuration store:", this.configuration)
}
public getCurrentValueFor(path: ReadonlyArray<string | number>): any | undefined {
@ -104,7 +119,6 @@ export default class EditLayerState {
public getStoreFor(path: ReadonlyArray<string | number>): UIEventSource<any | undefined> {
const store = new UIEventSource<any>(this.getCurrentValueFor(path))
store.addCallback((v) => {
console.log("UPdating store", path, v)
this.setValueAt(path, v)
})
return store
@ -156,7 +170,6 @@ export default class EditLayerState {
public setValueAt(path: ReadonlyArray<string | number>, v: any) {
let entry = this.configuration.data
console.log("Setting value", v, "to", path, "in entry", entry)
for (let i = 0; i < path.length - 1; i++) {
const breadcrumb = path[i]
if (entry[breadcrumb] === undefined) {

View file

@ -26,7 +26,6 @@
const subparts: ConfigMeta = state.getSchemaStartingWith(schema.path)
.filter(part => part.path.length - 1 === schema.path.length);
console.log("For ", schema.path, "got subparts", subparts)
/**
* Store the _indices_
*/

View file

@ -100,6 +100,7 @@
try {
onDestroy(state.register(path, tags.map(tgs => {
const v = tgs["value"];
console.log("Registering",path,"setting value to", v)
if(typeof v !== "string"){
return v
}
@ -116,6 +117,9 @@
}
}
if (schema.type === "number") {
if(v === ""){
return undefined
}
return Number(v)
}
if (isTranslation) {

View file

@ -30,6 +30,7 @@
types.splice(hasBooleanOption);
}
let lastIsString = false;
{
const types: string | string[] = Array.isArray(schema.type) ? schema.type[schema.type.length - 1].type : [];
@ -217,5 +218,4 @@
path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput>
{/each}
{/if}
{chosenOption}
</div>

View file

@ -3,15 +3,15 @@ import Constants from "../../Models/Constants"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
export default class StudioServer {
private _url: string
private readonly url: string
constructor(url: string) {
this._url = url
this.url = url
}
public async fetchLayerOverview(): Promise<Set<string>> {
const { allFiles } = <{ allFiles: string[] }>(
await Utils.downloadJson(this._url + "/overview")
await Utils.downloadJson(this.url + "/overview")
)
const layers = allFiles
.filter((f) => f.startsWith("layers/"))
@ -20,19 +20,27 @@ export default class StudioServer {
return new Set<string>(layers)
}
async fetchLayer(layerId: string, checkNew: boolean = false): Promise<LayerConfigJson> {
async fetchLayer(layerId: string): Promise<LayerConfigJson> {
try {
return await Utils.downloadJson(
this._url +
"/layers/" +
layerId +
"/" +
layerId +
".json" +
(checkNew ? ".new.json" : "")
this.url + "/layers/" + layerId + "/" + layerId + ".json"
)
} catch (e) {
return undefined
}
}
async updateLayer(config: LayerConfigJson) {
const id = config.id
if (id === undefined || id === "") {
return
}
await fetch(`${this.url}/layers/${id}/${id}.json`, {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify(config, null, " "),
})
}
}

View file

@ -82,6 +82,7 @@ const freeformSchema = <ConfigMeta[]> questionableTagRenderingSchemaRaw.filter(
<SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]}></SchemaBasedField>
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]}></SchemaBasedField>
<SchemaBasedField {state} path={[...path,"render"]} schema={topLevelItems["render"]}></SchemaBasedField>
{#each ($mappings ?? []) as mapping, i (mapping)}
<div class="flex interactive w-full">
@ -102,7 +103,12 @@ const freeformSchema = <ConfigMeta[]> questionableTagRenderingSchemaRaw.filter(
Add a mapping
</button>
<SchemaBasedField {state} path={[...path,"multiAnswer"]} schema={topLevelItems["multiAnswer"]}></SchemaBasedField>
<div class="border border-gray-200 border-dashed">
<h3>Text field and input element configuration</h3>
<Region {state} {path} configs={freeformSchema}/>
</div>
</div>
{/if}

View file

@ -14,8 +14,9 @@
import { OsmConnection } from "../Logic/Osm/OsmConnection";
import { QueryParameters } from "../Logic/Web/QueryParameters";
import layerSchemaRaw from "../../src/assets/schemas/layerconfigmeta.json";
export let studioUrl = "http://127.0.0.1:1235";
export let studioUrl = /*"https://studio.mapcomplete.org"; /*/ "http://127.0.0.1:1235"; //*/
const studio = new StudioServer(studioUrl);
let layers = UIEventSource.FromPromise(studio.fetchLayerOverview());
let state: undefined | "edit_layer" | "new_layer" | "edit_theme" | "new_theme" | "editing_layer" | "loading" = undefined;
@ -33,101 +34,121 @@
}, [layers]);
let editLayerState = new EditLayerState();
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
let editLayerState = new EditLayerState(layerSchema, studio);
function fetchIconDescription(layerId): any {
const icon = AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
console.log(icon);
return icon;
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
}
async function createNewLayer(){
state = "loading"
const id = newLayerId.data
const createdBy = osmConnection.userDetails.data.name
const loaded = await studio.fetchLayer(id, true)
initialLayerConfig = loaded ?? {id, credits: createdBy};
state = "editing_layer"}
let osmConnection = new OsmConnection( new OsmConnection({
async function createNewLayer() {
state = "loading";
const id = newLayerId.data;
const createdBy = osmConnection.userDetails.data.name;
try {
const loaded = await studio.fetchLayer(id);
initialLayerConfig = loaded ?? {
id, credits: createdBy,
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(
"oauth_token",
undefined,
"Used to complete the login"
),
}))
)
}));
</script>
<LoginToggle state={{osmConnection}}>
<div slot="not-logged-in" >
<NextButton clss="primary">
Please log in to use MapComplete Studio
</NextButton>
</div>
{#if state === undefined}
<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
<LoginToggle state={{osmConnection}} ignoreLoading={true}>
<div slot="not-logged-in">
<NextButton clss="primary">
Please log in to use MapComplete Studio
</NextButton>
</div>
{:else if state === "edit_layer"}
<div class="flex flex-wrap">
{#each Array.from($layers) as layerId}
<NextButton clss="small" on:click={async () => {
console.log("Editing layer",layerId)
{#if state === undefined}
<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>
</div>
{:else if state === "edit_layer"}
<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 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>
{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()}/>
{:else }
<NextButton clss="primary" on:click={() => createNewLayer()}>
Create layer {$newLayerId}
</NextButton>
{/if}
</div>
{#if $layerIdFeedback !== undefined}
<div class="alert">
{$layerIdFeedback}
{:else if state === "loading"}
<div class="w-8 h-8">
<Loading />
</div>
{:else }
<NextButton clss="primary" on:click={() => createNewLayer()}>
Create layer {$newLayerId}
</NextButton>
{:else if state === "editing_layer"}
<EditLayer {initialLayerConfig} state={editLayerState} />
{/if}
</div>
{:else if state === "loading"}
<div class="w-8 h-8">
<Loading />
</div>
{:else if state === "editing_layer"}
<EditLayer {initialLayerConfig} />
{/if}
</LoginToggle>