Add various improvements and fixes to studio, should fix #2055
This commit is contained in:
parent
3dc0014f35
commit
5d1c93396d
19 changed files with 531 additions and 418 deletions
|
@ -993,10 +993,6 @@ video {
|
|||
margin-right: 4rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
@ -1029,6 +1025,10 @@ video {
|
|||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
@ -4686,6 +4686,16 @@ textarea {
|
|||
color: black;
|
||||
}
|
||||
|
||||
h2.group {
|
||||
/* For flowbite accordions */
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.group button {
|
||||
/* For flowbite accordions */
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/************************* OTHER CATEGORIES ********************************/
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,40 +3,12 @@ import { Utils } from "../../Utils"
|
|||
export class ThemeMetaTagging {
|
||||
public static readonly themeName = "usersettings"
|
||||
|
||||
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
|
||||
feat.properties._description
|
||||
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
|
||||
?.at(1)
|
||||
)
|
||||
Utils.AddLazyProperty(
|
||||
feat.properties,
|
||||
"_d",
|
||||
() => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? ""
|
||||
)
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
|
||||
((feat) => {
|
||||
const e = document.createElement("div")
|
||||
e.innerHTML = feat.properties._d
|
||||
return Array.from(e.getElementsByTagName("a")).filter(
|
||||
(a) => a.href.match(/mastodon|en.osm.town/) !== null
|
||||
)[0]?.href
|
||||
})(feat)
|
||||
)
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
|
||||
((feat) => {
|
||||
const e = document.createElement("div")
|
||||
e.innerHTML = feat.properties._d
|
||||
return Array.from(e.getElementsByTagName("a")).filter(
|
||||
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
|
||||
)[0]?.href
|
||||
})(feat)
|
||||
)
|
||||
Utils.AddLazyProperty(
|
||||
feat.properties,
|
||||
"_mastodon_candidate",
|
||||
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
|
||||
)
|
||||
feat.properties["__current_backgroun"] = "initial_value"
|
||||
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
|
||||
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
|
||||
feat.properties['__current_backgroun'] = 'initial_value'
|
||||
}
|
||||
}
|
|
@ -73,15 +73,20 @@ export abstract class DesugaringStep<T> extends Conversion<T, T> {}
|
|||
export class Pipe<TIn, TInter, TOut> extends Conversion<TIn, TOut> {
|
||||
private readonly _step0: Conversion<TIn, TInter>
|
||||
private readonly _step1: Conversion<TInter, TOut>
|
||||
private readonly _failfast: boolean
|
||||
|
||||
constructor(step0: Conversion<TIn, TInter>, step1: Conversion<TInter, TOut>) {
|
||||
constructor(step0: Conversion<TIn, TInter>, step1: Conversion<TInter, TOut>, failfast = false) {
|
||||
super("Merges two steps with different types", [], `Pipe(${step0.name}, ${step1.name})`)
|
||||
this._step0 = step0
|
||||
this._step1 = step1
|
||||
this._failfast = failfast
|
||||
}
|
||||
|
||||
convert(json: TIn, context: ConversionContext): TOut {
|
||||
const r0 = this._step0.convert(json, context.inOperation(this._step0.name))
|
||||
if(context.hasErrors() && this._failfast){
|
||||
return undefined
|
||||
}
|
||||
return this._step1.convert(r0, context.inOperation(this._step1.name))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import DependencyCalculator from "../DependencyCalculator"
|
|||
import { AddContextToTranslations } from "./AddContextToTranslations"
|
||||
import ValidationUtils from "./ValidationUtils"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
import { PrevalidateTheme } from "./Validation"
|
||||
|
||||
class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJson[]> {
|
||||
private readonly _state: DesugaringContext
|
||||
|
@ -664,7 +665,6 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
|
|||
) {
|
||||
super(
|
||||
"Fully prepares and expands a theme",
|
||||
|
||||
new AddContextToTranslationsInLayout(),
|
||||
new PreparePersonalTheme(state),
|
||||
new WarnForUnsubstitutedLayersInTheme(),
|
||||
|
|
|
@ -36,7 +36,7 @@ class ValidateLanguageCompleteness extends DesugaringStep<LayoutConfig> {
|
|||
super(
|
||||
"Checks that the given object is fully translated in the specified languages",
|
||||
[],
|
||||
"ValidateLanguageCompleteness"
|
||||
"ValidateLanguageCompleteness",
|
||||
)
|
||||
this._languages = languages ?? ["en"]
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ class ValidateLanguageCompleteness extends DesugaringStep<LayoutConfig> {
|
|||
.filter(
|
||||
(t) =>
|
||||
t.tr.translations[neededLanguage] === undefined &&
|
||||
t.tr.translations["*"] === undefined
|
||||
t.tr.translations["*"] === undefined,
|
||||
)
|
||||
.forEach((missing) => {
|
||||
context
|
||||
|
@ -61,7 +61,7 @@ class ValidateLanguageCompleteness extends DesugaringStep<LayoutConfig> {
|
|||
", but it lacks a translation for " +
|
||||
missing.context +
|
||||
".\n\tThe known translation is " +
|
||||
missing.tr.textFor("en")
|
||||
missing.tr.textFor("en"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ export class DoesImageExist extends DesugaringStep<string> {
|
|||
constructor(
|
||||
knownImagePaths: Set<string>,
|
||||
checkExistsSync: (path: string) => boolean = undefined,
|
||||
ignore?: Set<string>
|
||||
ignore?: Set<string>,
|
||||
) {
|
||||
super("Checks if an image exists", [], "DoesImageExist")
|
||||
this._ignore = ignore
|
||||
|
@ -114,15 +114,15 @@ export class DoesImageExist extends DesugaringStep<string> {
|
|||
if (!this._knownImagePaths.has(image)) {
|
||||
if (this.doesPathExist === undefined) {
|
||||
context.err(
|
||||
`Image with path ${image} not found or not attributed; it is used in ${context}`
|
||||
`Image with path ${image} not found or not attributed; it is used in ${context}`,
|
||||
)
|
||||
} else if (!this.doesPathExist(image)) {
|
||||
context.err(
|
||||
`Image with path ${image} does not exist.\n Check for typo's and missing directories in the path.`
|
||||
`Image with path ${image} does not exist.\n Check for typo's and missing directories in the path.`,
|
||||
)
|
||||
} else {
|
||||
context.err(
|
||||
`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`
|
||||
`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
doesImageExist: DoesImageExist,
|
||||
path: string,
|
||||
isBuiltin: boolean,
|
||||
sharedTagRenderings?: Set<string>
|
||||
sharedTagRenderings?: Set<string>,
|
||||
) {
|
||||
super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme")
|
||||
this._validateImage = doesImageExist
|
||||
|
@ -166,14 +166,14 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
context.err(
|
||||
"The theme " +
|
||||
json.id +
|
||||
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) "
|
||||
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) ",
|
||||
)
|
||||
}
|
||||
if (json["roamingRenderings"] !== undefined) {
|
||||
context.err(
|
||||
"Theme " +
|
||||
json.id +
|
||||
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead"
|
||||
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -194,7 +194,7 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
remoteImage.path +
|
||||
" in theme " +
|
||||
json.id +
|
||||
", please download it."
|
||||
", please download it.",
|
||||
)
|
||||
}
|
||||
for (const image of images) {
|
||||
|
@ -210,7 +210,7 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
|
||||
const filename = this._path.substring(
|
||||
this._path.lastIndexOf("/") + 1,
|
||||
this._path.length - 5
|
||||
this._path.length - 5,
|
||||
)
|
||||
if (theme.id !== filename) {
|
||||
context.err(
|
||||
|
@ -220,7 +220,7 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
filename +
|
||||
" (" +
|
||||
this._path +
|
||||
")"
|
||||
")",
|
||||
)
|
||||
}
|
||||
this._validateImage.convert(theme.icon, context.enter("icon"))
|
||||
|
@ -228,13 +228,13 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"]))
|
||||
if (dups.length > 0) {
|
||||
context.err(
|
||||
`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`
|
||||
`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`,
|
||||
)
|
||||
}
|
||||
if (json["mustHaveLanguage"] !== undefined) {
|
||||
new ValidateLanguageCompleteness(...json["mustHaveLanguage"]).convert(
|
||||
theme,
|
||||
context
|
||||
context,
|
||||
)
|
||||
}
|
||||
if (!json.hideFromOverview && theme.id !== "personal" && this._isBuiltin) {
|
||||
|
@ -242,7 +242,7 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
const targetLanguage = theme.title.SupportedLanguages()[0]
|
||||
if (targetLanguage !== "en") {
|
||||
context.err(
|
||||
`TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key`
|
||||
`TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key`,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -286,7 +286,7 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
.err(
|
||||
`This layer ID is not known: ${backgroundId}. Perhaps you meant one of ${nearby
|
||||
.slice(0, 5)
|
||||
.join(", ")}`
|
||||
.join(", ")}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -309,7 +309,7 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
|
|||
doesImageExist: DoesImageExist,
|
||||
path: string,
|
||||
isBuiltin: boolean,
|
||||
sharedTagRenderings?: Set<string>
|
||||
sharedTagRenderings?: Set<string>,
|
||||
) {
|
||||
super(
|
||||
"Validates a theme and the contained layers",
|
||||
|
@ -319,10 +319,10 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
|
|||
new Each(
|
||||
new Bypass(
|
||||
(layer) => Constants.added_by_default.indexOf(<any>layer.id) < 0,
|
||||
new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true)
|
||||
)
|
||||
)
|
||||
)
|
||||
new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -332,7 +332,7 @@ class OverrideShadowingCheck extends DesugaringStep<LayoutConfigJson> {
|
|||
super(
|
||||
"Checks that an 'overrideAll' does not override a single override",
|
||||
[],
|
||||
"OverrideShadowingCheck"
|
||||
"OverrideShadowingCheck",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -378,6 +378,9 @@ class MiscThemeChecks extends DesugaringStep<LayoutConfigJson> {
|
|||
if (json.id !== "personal" && (json.layers === undefined || json.layers.length === 0)) {
|
||||
context.err("The theme " + json.id + " has no 'layers' defined")
|
||||
}
|
||||
if (!Array.isArray(json.layers)) {
|
||||
context.enter("layers").err("The 'layers'-field should be an array, but it is not. Did you pase a layer identifier and forget to add the '[' and ']'?")
|
||||
}
|
||||
if (json.socialImage === "") {
|
||||
context.warn("Social image for theme " + json.id + " is the emtpy string")
|
||||
}
|
||||
|
@ -406,7 +409,7 @@ class MiscThemeChecks extends DesugaringStep<LayoutConfigJson> {
|
|||
context
|
||||
.enter("overideAll")
|
||||
.err(
|
||||
"'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them."
|
||||
"'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them.",
|
||||
)
|
||||
}
|
||||
return json
|
||||
|
@ -418,7 +421,7 @@ export class PrevalidateTheme extends Fuse<LayoutConfigJson> {
|
|||
super(
|
||||
"Various consistency checks on the raw JSON",
|
||||
new MiscThemeChecks(),
|
||||
new OverrideShadowingCheck()
|
||||
new OverrideShadowingCheck(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -428,7 +431,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
|
|||
super(
|
||||
"The `if`-part in a mapping might set some keys. Those keys are not allowed to be set in the `addExtraTags`, as this might result in conflicting values",
|
||||
[],
|
||||
"DetectConflictingAddExtraTags"
|
||||
"DetectConflictingAddExtraTags",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -455,7 +458,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
|
|||
.enters("mappings", i)
|
||||
.err(
|
||||
"AddExtraTags overrides a key that is set in the `if`-clause of this mapping. Selecting this answer might thus first set one value (needed to match as answer) and then override it with a different value, resulting in an unsaveable question. The offending `addExtraTags` is " +
|
||||
duplicateKeys.join(", ")
|
||||
duplicateKeys.join(", "),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -473,13 +476,13 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
|
|||
super(
|
||||
"A tagRendering might set a freeform key (e.g. `name` and have an option that _should_ erase this name, e.g. `noname=yes`). Under normal circumstances, every mapping/freeform should affect all touched keys",
|
||||
[],
|
||||
"DetectNonErasedKeysInMappings"
|
||||
"DetectNonErasedKeysInMappings",
|
||||
)
|
||||
}
|
||||
|
||||
convert(
|
||||
json: QuestionableTagRenderingConfigJson,
|
||||
context: ConversionContext
|
||||
context: ConversionContext,
|
||||
): QuestionableTagRenderingConfigJson {
|
||||
if (json.multiAnswer) {
|
||||
// No need to check this here, this has its own validation
|
||||
|
@ -534,7 +537,7 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
|
|||
.warn(
|
||||
"The freeform block does not modify the key `" +
|
||||
neededKey +
|
||||
"` which is set in a mapping. Use `addExtraTags` to overwrite it"
|
||||
"` which is set in a mapping. Use `addExtraTags` to overwrite it",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -553,7 +556,7 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
|
|||
.warn(
|
||||
"This mapping does not modify the key `" +
|
||||
neededKey +
|
||||
"` which is set in a mapping or by the freeform block. Use `addExtraTags` to overwrite it"
|
||||
"` which is set in a mapping or by the freeform block. Use `addExtraTags` to overwrite it",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -570,7 +573,7 @@ export class DetectMappingsShadowedByCondition extends DesugaringStep<TagRenderi
|
|||
super(
|
||||
"Checks that, if the tagrendering has a condition, that a mapping is not contradictory to it, i.e. that there are no dead mappings",
|
||||
[],
|
||||
"DetectMappingsShadowedByCondition"
|
||||
"DetectMappingsShadowedByCondition",
|
||||
)
|
||||
this._forceError = forceError
|
||||
}
|
||||
|
@ -642,7 +645,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
|
|||
* DetectShadowedMappings.extractCalculatedTagNames({calculatedTags: ["_abc=js()"]}) // => ["_abc"]
|
||||
*/
|
||||
private static extractCalculatedTagNames(
|
||||
layerConfig?: LayerConfigJson | { calculatedTags: string[] }
|
||||
layerConfig?: LayerConfigJson | { calculatedTags: string[] },
|
||||
) {
|
||||
return (
|
||||
layerConfig?.calculatedTags?.map((ct) => {
|
||||
|
@ -728,7 +731,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
|
|||
json.mappings[i]["hideInAnswer"] !== true
|
||||
) {
|
||||
context.warn(
|
||||
`Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`
|
||||
`Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`,
|
||||
)
|
||||
} else if (doesMatch) {
|
||||
// The current mapping is shadowed!
|
||||
|
@ -736,7 +739,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
|
|||
The mapping ${parsedConditions[i].asHumanString(
|
||||
false,
|
||||
false,
|
||||
{}
|
||||
{},
|
||||
)} is fully matched by a previous mapping (namely ${j}), which matches:
|
||||
${parsedConditions[j].asHumanString(false, false, {})}.
|
||||
|
||||
|
@ -764,7 +767,7 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
|
|||
super(
|
||||
"Checks that 'then'clauses in mappings don't have images, but use 'icon' instead",
|
||||
[],
|
||||
"DetectMappingsWithImages"
|
||||
"DetectMappingsWithImages",
|
||||
)
|
||||
this._doesImageExist = doesImageExist
|
||||
}
|
||||
|
@ -804,14 +807,14 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
|
|||
if (!ignore) {
|
||||
ctx.err(
|
||||
`A mapping has an image in the 'then'-clause. Remove the image there and use \`"icon": <your-image>\` instead. The images found are ${images.join(
|
||||
", "
|
||||
)}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged`
|
||||
", ",
|
||||
)}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged`,
|
||||
)
|
||||
} else {
|
||||
ctx.info(
|
||||
`Ignored image ${images.join(
|
||||
", "
|
||||
)} in 'then'-clause of a mapping as this check has been disabled`
|
||||
", ",
|
||||
)} in 'then'-clause of a mapping as this check has been disabled`,
|
||||
)
|
||||
|
||||
for (const image of images) {
|
||||
|
@ -832,7 +835,7 @@ class ValidatePossibleLinks extends DesugaringStep<string | Record<string, strin
|
|||
super(
|
||||
"Given a possible set of translations, validates that <a href=... target='_blank'> does have `rel='noopener'` set",
|
||||
[],
|
||||
"ValidatePossibleLinks"
|
||||
"ValidatePossibleLinks",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -862,21 +865,21 @@ class ValidatePossibleLinks extends DesugaringStep<string | Record<string, strin
|
|||
|
||||
convert(
|
||||
json: string | Record<string, string>,
|
||||
context: ConversionContext
|
||||
context: ConversionContext,
|
||||
): string | Record<string, string> {
|
||||
if (typeof json === "string") {
|
||||
if (this.isTabnabbingProne(json)) {
|
||||
context.err(
|
||||
"The string " +
|
||||
json +
|
||||
" has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping"
|
||||
" has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
for (const k in json) {
|
||||
if (this.isTabnabbingProne(json[k])) {
|
||||
context.err(
|
||||
`The translation for ${k} '${json[k]}' has a link targeting \`_blank\`, but it doesn't have \`rel='noopener'\` set. This gives rise to reverse tabnapping`
|
||||
`The translation for ${k} '${json[k]}' has a link targeting \`_blank\`, but it doesn't have \`rel='noopener'\` set. This gives rise to reverse tabnapping`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -894,7 +897,7 @@ class CheckTranslation extends DesugaringStep<Translatable> {
|
|||
super(
|
||||
"Checks that a translation is valid and internally consistent",
|
||||
["*"],
|
||||
"CheckTranslation"
|
||||
"CheckTranslation",
|
||||
)
|
||||
this._allowUndefined = allowUndefined
|
||||
}
|
||||
|
@ -935,6 +938,7 @@ class CheckTranslation extends DesugaringStep<Translatable> {
|
|||
|
||||
class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
||||
private readonly _layerConfig: LayerConfigJson
|
||||
|
||||
constructor(layerConfig?: LayerConfigJson) {
|
||||
super("Miscellaneous checks on the tagrendering", ["special"], "MiscTagRenderingChecks")
|
||||
this._layerConfig = layerConfig
|
||||
|
@ -942,17 +946,17 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
|
||||
convert(
|
||||
json: TagRenderingConfigJson | QuestionableTagRenderingConfigJson,
|
||||
context: ConversionContext
|
||||
context: ConversionContext,
|
||||
): TagRenderingConfigJson {
|
||||
if (json["special"] !== undefined) {
|
||||
context.err(
|
||||
'Detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`'
|
||||
"Detected `special` on the top level. Did you mean `{\"render\":{ \"special\": ... }}`",
|
||||
)
|
||||
}
|
||||
|
||||
if (Object.keys(json).length === 1 && typeof json["render"] === "string") {
|
||||
context.warn(
|
||||
`use the content directly instead of {render: ${JSON.stringify(json["render"])}}`
|
||||
`use the content directly instead of {render: ${JSON.stringify(json["render"])}}`,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -964,7 +968,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
const mapping: MappingConfigJson = json.mappings[i]
|
||||
CheckTranslation.noUndefined.convert(
|
||||
mapping.then,
|
||||
context.enters("mappings", i, "then")
|
||||
context.enters("mappings", i, "then"),
|
||||
)
|
||||
if (!mapping.if) {
|
||||
console.log(
|
||||
|
@ -973,7 +977,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
"if",
|
||||
mapping.if,
|
||||
context.path.join("."),
|
||||
mapping.then
|
||||
mapping.then,
|
||||
)
|
||||
context.enters("mappings", i, "if").err("No `if` is defined")
|
||||
}
|
||||
|
@ -983,7 +987,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
context
|
||||
.enters("mappings", i, "addExtraTags", j)
|
||||
.err(
|
||||
"Detected a 'null' or 'undefined' value. Either specify a tag or delete this item"
|
||||
"Detected a 'null' or 'undefined' value. Either specify a tag or delete this item",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -994,18 +998,18 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
context
|
||||
.enters("mappings", i, "then")
|
||||
.warn(
|
||||
"A mapping should not start with 'yes' or 'no'. If the attribute is known, it will only show 'yes' or 'no' <i>without</i> the question, resulting in a weird phrasing in the information box"
|
||||
"A mapping should not start with 'yes' or 'no'. If the attribute is known, it will only show 'yes' or 'no' <i>without</i> the question, resulting in a weird phrasing in the information box",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (json["group"]) {
|
||||
context.err('Groups are deprecated, use `"label": ["' + json["group"] + '"]` instead')
|
||||
context.err("Groups are deprecated, use `\"label\": [\"" + json["group"] + "\"]` instead")
|
||||
}
|
||||
|
||||
if (json["question"] && json.freeform?.key === undefined && json.mappings === undefined) {
|
||||
context.err(
|
||||
"A question is defined, but no mappings nor freeform (key) are. Add at least one of them"
|
||||
"A question is defined, but no mappings nor freeform (key) are. Add at least one of them",
|
||||
)
|
||||
}
|
||||
if (json["question"] && !json.freeform && (json.mappings?.length ?? 0) == 1) {
|
||||
|
@ -1015,7 +1019,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
context
|
||||
.enter("questionHint")
|
||||
.err(
|
||||
"A questionHint is defined, but no question is given. As such, the questionHint will never be shown"
|
||||
"A questionHint is defined, but no question is given. As such, the questionHint will never be shown",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1023,7 +1027,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
context
|
||||
.enters("icon", "size")
|
||||
.err(
|
||||
"size is not a valid attribute. Did you mean 'class'? Class can be one of `small`, `medium` or `large`"
|
||||
"size is not a valid attribute. Did you mean 'class'? Class can be one of `small`, `medium` or `large`",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1036,7 +1040,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
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)
|
||||
|
@ -1067,7 +1071,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
const keyFirstArg = ["canonical", "fediverse_link", "translated"]
|
||||
if (
|
||||
keyFirstArg.some(
|
||||
(funcName) => txt.indexOf(`{${funcName}(${json.freeform.key}`) >= 0
|
||||
(funcName) => txt.indexOf(`{${funcName}(${json.freeform.key}`) >= 0,
|
||||
)
|
||||
) {
|
||||
continue
|
||||
|
@ -1091,7 +1095,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
context
|
||||
.enter("render")
|
||||
.err(
|
||||
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. Did you perhaps forget to set "freeform.type: 'wikidata'"?`
|
||||
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. Did you perhaps forget to set "freeform.type: 'wikidata'"?`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
@ -1103,7 +1107,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
context
|
||||
.enter("render")
|
||||
.err(
|
||||
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. However, it does contain ${json.freeform.key} without braces. Did you forget the braces?\n\tThe current text is ${txt}`
|
||||
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. However, it does contain ${json.freeform.key} without braces. Did you forget the braces?\n\tThe current text is ${txt}`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
@ -1111,7 +1115,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
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!\n\tThe current text is ${txt}`
|
||||
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. This is a bug, as this rendering should show exactly this freeform key!\n\tThe current text is ${txt}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1126,22 +1130,22 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
.enters("freeform", "type")
|
||||
.err(
|
||||
"No entry found in the 'Name Suggestion Index'. None of the 'osmSource'-tags match an entry in the NSI.\n\tOsmSource-tags are " +
|
||||
tags.map((t) => new Tag(t.key, t.value).asHumanString()).join(" ; ")
|
||||
tags.map((t) => new Tag(t.key, t.value).asHumanString()).join(" ; "),
|
||||
)
|
||||
}
|
||||
} else if (json.freeform.type === "nsi") {
|
||||
context
|
||||
.enters("freeform", "type")
|
||||
.warn(
|
||||
"No need to explicitly set type to 'NSI', autodetected based on freeform type"
|
||||
"No need to explicitly set type to 'NSI', autodetected based on freeform type",
|
||||
)
|
||||
}
|
||||
}
|
||||
if (json.render && json["question"] && json.freeform === undefined) {
|
||||
context.err(
|
||||
`Detected a tagrendering which takes input without freeform key in ${context}; the question is ${new Translation(
|
||||
json["question"]
|
||||
).textFor("en")}`
|
||||
json["question"],
|
||||
).textFor("en")}`,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1154,7 +1158,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
"Unknown type: " +
|
||||
freeformType +
|
||||
"; try one of " +
|
||||
Validators.availableTypes.join(", ")
|
||||
Validators.availableTypes.join(", "),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1192,7 +1196,7 @@ export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
|
|||
new On("question", new ValidatePossibleLinks()),
|
||||
new On("questionHint", new ValidatePossibleLinks()),
|
||||
new On("mappings", new Each(new On("then", new ValidatePossibleLinks()))),
|
||||
new MiscTagRenderingChecks(layerConfig)
|
||||
new MiscTagRenderingChecks(layerConfig),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1211,7 +1215,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
path: string,
|
||||
isBuiltin: boolean,
|
||||
doesImageExist: DoesImageExist,
|
||||
studioValidations: boolean
|
||||
studioValidations: boolean,
|
||||
) {
|
||||
super("Runs various checks against common mistakes for a layer", [], "PrevalidateLayer")
|
||||
this._path = path
|
||||
|
@ -1237,7 +1241,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
context
|
||||
.enter("source")
|
||||
.err(
|
||||
"No source section is defined; please define one as data is not loaded otherwise"
|
||||
"No source section is defined; please define one as data is not loaded otherwise",
|
||||
)
|
||||
} else {
|
||||
if (json.source === "special" || json.source === "special:library") {
|
||||
|
@ -1245,7 +1249,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
context
|
||||
.enters("source", "osmTags")
|
||||
.err(
|
||||
"No osmTags defined in the source section - these should always be present, even for geojson layer"
|
||||
"No osmTags defined in the source section - these should always be present, even for geojson layer",
|
||||
)
|
||||
} else {
|
||||
const osmTags = TagUtils.Tag(json.source["osmTags"], context + "source.osmTags")
|
||||
|
@ -1254,7 +1258,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
.enters("source", "osmTags")
|
||||
.err(
|
||||
"The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" +
|
||||
osmTags.asHumanString(false, false, {})
|
||||
osmTags.asHumanString(false, false, {}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1283,7 +1287,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") +
|
||||
" but got '" +
|
||||
json.syncSelection +
|
||||
"'"
|
||||
"'",
|
||||
)
|
||||
}
|
||||
if (json["pointRenderings"]?.length > 0) {
|
||||
|
@ -1302,7 +1306,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
}
|
||||
|
||||
json.pointRendering?.forEach((pr, i) =>
|
||||
this._validatePointRendering.convert(pr, context.enters("pointeRendering", i))
|
||||
this._validatePointRendering.convert(pr, context.enters("pointeRendering", i)),
|
||||
)
|
||||
|
||||
if (json["mapRendering"]) {
|
||||
|
@ -1320,7 +1324,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
context.err(
|
||||
"Layer " +
|
||||
json.id +
|
||||
" uses 'special' as source.osmTags. However, this layer is not a priviliged layer"
|
||||
" uses 'special' as source.osmTags. However, this layer is not a priviliged layer",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1335,19 +1339,19 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
context
|
||||
.enter("title")
|
||||
.err(
|
||||
"This layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error."
|
||||
"This layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error.",
|
||||
)
|
||||
}
|
||||
if (json.title === null) {
|
||||
context.info(
|
||||
"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")
|
||||
<string[]>json.tagRenderings.filter((tr) => typeof tr === "string"),
|
||||
)
|
||||
for (let i = 0; i < json.tagRenderings.length; i++) {
|
||||
const tagRendering = json.tagRenderings[i]
|
||||
|
@ -1377,7 +1381,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
{
|
||||
// duplicate ids in tagrenderings check
|
||||
const duplicates = Utils.NoNull(
|
||||
Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"])))
|
||||
Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"]))),
|
||||
)
|
||||
if (duplicates.length > 0) {
|
||||
// It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list
|
||||
|
@ -1388,8 +1392,8 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
duplicates.join(", ") +
|
||||
"\n" +
|
||||
JSON.stringify(
|
||||
json.tagRenderings.filter((tr) => duplicates.indexOf(tr["id"]) >= 0)
|
||||
)
|
||||
json.tagRenderings.filter((tr) => duplicates.indexOf(tr["id"]) >= 0),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1423,7 +1427,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
context.err(
|
||||
"Layer " +
|
||||
json.id +
|
||||
'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": <tags>}\' instead of "overpassTags": <tags> (note: this isn\'t your fault, the custom theme generator still spits out the old format)'
|
||||
"still uses the old 'overpassTags'-format. Please use \"source\": {\"osmTags\": <tags>}' instead of \"overpassTags\": <tags> (note: this isn't your fault, the custom theme generator still spits out the old format)",
|
||||
)
|
||||
}
|
||||
const forbiddenTopLevel = [
|
||||
|
@ -1443,7 +1447,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
}
|
||||
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
|
||||
context.err(
|
||||
"Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'"
|
||||
"Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1462,7 +1466,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
"Layer is in an incorrect place. The path is " +
|
||||
this._path +
|
||||
", but expected " +
|
||||
expected
|
||||
expected,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1480,13 +1484,13 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
.enter(["tagRenderings", ...emptyIndexes])
|
||||
.err(
|
||||
`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${emptyIndexes.join(
|
||||
","
|
||||
)}])`
|
||||
",",
|
||||
)}])`,
|
||||
)
|
||||
}
|
||||
|
||||
const duplicateIds = Utils.Duplicates(
|
||||
(json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions")
|
||||
(json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions"),
|
||||
)
|
||||
if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
|
||||
context
|
||||
|
@ -1510,7 +1514,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
if (json.tagRenderings !== undefined) {
|
||||
new On(
|
||||
"tagRenderings",
|
||||
new Each(new ValidateTagRenderings(json, this._doesImageExist))
|
||||
new Each(new ValidateTagRenderings(json, this._doesImageExist)),
|
||||
).convert(json, context)
|
||||
}
|
||||
|
||||
|
@ -1537,7 +1541,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
context
|
||||
.enters("pointRendering", i, "marker", indexM, "icon", "condition")
|
||||
.err(
|
||||
"Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead."
|
||||
"Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1577,7 +1581,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
"This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
|
||||
tags.asHumanString(false, false, {}) +
|
||||
"\n The required tags are: " +
|
||||
baseTags.asHumanString(false, false, {})
|
||||
baseTags.asHumanString(false, false, {}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1594,7 +1598,7 @@ export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
|
|||
isBuiltin: boolean,
|
||||
doesImageExist: DoesImageExist,
|
||||
studioValidations: boolean = false,
|
||||
skipDefaultLayers: boolean = false
|
||||
skipDefaultLayers: boolean = false,
|
||||
) {
|
||||
super("Thin wrapper around 'ValidateLayer", [], "ValidateLayerConfig")
|
||||
this.validator = new ValidateLayer(
|
||||
|
@ -1602,7 +1606,7 @@ export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
|
|||
isBuiltin,
|
||||
doesImageExist,
|
||||
studioValidations,
|
||||
skipDefaultLayers
|
||||
skipDefaultLayers,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1630,7 +1634,7 @@ class ValidatePointRendering extends DesugaringStep<PointRenderingConfigJson> {
|
|||
context
|
||||
.enter("markers")
|
||||
.err(
|
||||
`Detected a field 'markerS' in pointRendering. It is written as a singular case`
|
||||
`Detected a field 'markerS' in pointRendering. It is written as a singular case`,
|
||||
)
|
||||
}
|
||||
if (json.marker && !Array.isArray(json.marker)) {
|
||||
|
@ -1640,7 +1644,7 @@ class ValidatePointRendering extends DesugaringStep<PointRenderingConfigJson> {
|
|||
context
|
||||
.enter("location")
|
||||
.err(
|
||||
"A pointRendering should have at least one 'location' to defined where it should be rendered. "
|
||||
"A pointRendering should have at least one 'location' to defined where it should be rendered. ",
|
||||
)
|
||||
}
|
||||
return json
|
||||
|
@ -1659,26 +1663,26 @@ export class ValidateLayer extends Conversion<
|
|||
isBuiltin: boolean,
|
||||
doesImageExist: DoesImageExist,
|
||||
studioValidations: boolean = false,
|
||||
skipDefaultLayers: boolean = false
|
||||
skipDefaultLayers: boolean = false,
|
||||
) {
|
||||
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer")
|
||||
this._prevalidation = new PrevalidateLayer(
|
||||
path,
|
||||
isBuiltin,
|
||||
doesImageExist,
|
||||
studioValidations
|
||||
studioValidations,
|
||||
)
|
||||
this._skipDefaultLayers = skipDefaultLayers
|
||||
}
|
||||
|
||||
convert(
|
||||
json: LayerConfigJson,
|
||||
context: ConversionContext
|
||||
context: ConversionContext,
|
||||
): { parsed: LayerConfig; raw: LayerConfigJson } {
|
||||
context = context.inOperation(this.name)
|
||||
if (typeof json === "string") {
|
||||
context.err(
|
||||
`Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`
|
||||
`Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`,
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
@ -1709,7 +1713,7 @@ export class ValidateLayer extends Conversion<
|
|||
context
|
||||
.enters("calculatedTags", i)
|
||||
.err(
|
||||
`Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`
|
||||
`Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1757,7 +1761,7 @@ export class ValidateLayer extends Conversion<
|
|||
context
|
||||
.enters("allowMove", "enableAccuracy")
|
||||
.err(
|
||||
"`enableAccuracy` is written with two C in the first occurrence and only one in the last"
|
||||
"`enableAccuracy` is written with two C in the first occurrence and only one in the last",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1788,8 +1792,8 @@ export class ValidateFilter extends DesugaringStep<FilterConfigJson> {
|
|||
.enters("fields", i)
|
||||
.err(
|
||||
`Invalid filter: ${type} is not a valid textfield type.\n\tTry one of ${Array.from(
|
||||
Validators.availableTypes
|
||||
).join(",")}`
|
||||
Validators.availableTypes,
|
||||
).join(",")}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1806,13 +1810,13 @@ export class DetectDuplicateFilters extends DesugaringStep<{
|
|||
super(
|
||||
"Tries to detect layers where a shared filter can be used (or where similar filters occur)",
|
||||
[],
|
||||
"DetectDuplicateFilters"
|
||||
"DetectDuplicateFilters",
|
||||
)
|
||||
}
|
||||
|
||||
convert(
|
||||
json: { layers: LayerConfigJson[]; themes: LayoutConfigJson[] },
|
||||
context: ConversionContext
|
||||
context: ConversionContext,
|
||||
): { layers: LayerConfigJson[]; themes: LayoutConfigJson[] } {
|
||||
const { layers, themes } = json
|
||||
const perOsmTag = new Map<
|
||||
|
@ -1876,7 +1880,7 @@ export class DetectDuplicateFilters extends DesugaringStep<{
|
|||
filter: FilterConfigJson
|
||||
}[]
|
||||
>,
|
||||
layout?: LayoutConfigJson | undefined
|
||||
layout?: LayoutConfigJson | undefined,
|
||||
): void {
|
||||
if (layer.filter === undefined || layer.filter === null) {
|
||||
return
|
||||
|
@ -1916,7 +1920,7 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
|
|||
super(
|
||||
"Detects mappings which have identical (english) names or identical mappings.",
|
||||
["presets"],
|
||||
"DetectDuplicatePresets"
|
||||
"DetectDuplicatePresets",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1927,13 +1931,13 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
|
|||
if (new Set(enNames).size != enNames.length) {
|
||||
const dups = Utils.Duplicates(enNames)
|
||||
const layersWithDup = json.layers.filter((l) =>
|
||||
l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0)
|
||||
l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0),
|
||||
)
|
||||
const layerIds = layersWithDup.map((l) => l.id)
|
||||
context.err(
|
||||
`This theme has multiple presets which are named:${dups}, namely layers ${layerIds.join(
|
||||
", "
|
||||
)} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`
|
||||
", ",
|
||||
)} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1948,17 +1952,17 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
|
|||
Utils.SameObject(presetATags, presetBTags) &&
|
||||
Utils.sameList(
|
||||
presetA.preciseInput.snapToLayers,
|
||||
presetB.preciseInput.snapToLayers
|
||||
presetB.preciseInput.snapToLayers,
|
||||
)
|
||||
) {
|
||||
context.err(
|
||||
`This theme has multiple presets with the same tags: ${presetATags.asHumanString(
|
||||
false,
|
||||
false,
|
||||
{}
|
||||
{},
|
||||
)}, namely the preset '${presets[i].title.textFor("en")}' and '${presets[
|
||||
j
|
||||
].title.textFor("en")}'`
|
||||
].title.textFor("en")}'`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1983,13 +1987,13 @@ export class ValidateThemeEnsemble extends Conversion<
|
|||
super(
|
||||
"Validates that all themes together are logical, i.e. no duplicate ids exists within (overriden) themes",
|
||||
[],
|
||||
"ValidateThemeEnsemble"
|
||||
"ValidateThemeEnsemble",
|
||||
)
|
||||
}
|
||||
|
||||
convert(
|
||||
json: LayoutConfig[],
|
||||
context: ConversionContext
|
||||
context: ConversionContext,
|
||||
): Map<
|
||||
string,
|
||||
{
|
||||
|
@ -2044,7 +2048,7 @@ export class ValidateThemeEnsemble extends Conversion<
|
|||
"' is found in multiple themes with different tag definitions:",
|
||||
"\t In theme " + oldTheme + ":\t" + oldTags.asHumanString(false, false, {}),
|
||||
"\tIn theme " + theme.id + ":\t" + tags.asHumanString(false, false, {}),
|
||||
].join("\n")
|
||||
].join("\n"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@ export interface LayoutConfigJson {
|
|||
* Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)
|
||||
*
|
||||
* Type: icon
|
||||
* suggestions: return Constants.defaultPinIcons.map(i => ({if: "value="+i, then: i, icon: i}))
|
||||
* group: basic
|
||||
*
|
||||
*/
|
||||
|
@ -156,6 +157,7 @@ export interface LayoutConfigJson {
|
|||
* type: layer[]
|
||||
* types: hidden | layer | hidden
|
||||
* group: layers
|
||||
* title: value["builtin"] ?? value["id"] ?? value
|
||||
* suggestions: return Array.from(layers.keys()).map(key => ({if: "value="+key, then: "<b>"+key+"</b> (builtin) - "+layers.get(key).description}))
|
||||
*
|
||||
* A theme must contain at least one layer.
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<Accordion>
|
||||
<AccordionItem open={expanded} paddingDefault="p-0" inactiveClass="text-black">
|
||||
<span slot="header" class="p-2 text-base">
|
||||
<span slot="header" class="p-2 text-base w-full">
|
||||
<slot name="header" />
|
||||
</span>
|
||||
<div class="low-interaction rounded-b p-2">
|
||||
|
|
|
@ -373,7 +373,6 @@
|
|||
{feedback}
|
||||
{unit}
|
||||
{state}
|
||||
{extraTags}
|
||||
feature={selectedElement}
|
||||
value={freeformInput}
|
||||
unvalidatedText={freeformInputUnvalidated}
|
||||
|
@ -418,7 +417,6 @@
|
|||
{feedback}
|
||||
{unit}
|
||||
{state}
|
||||
{extraTags}
|
||||
feature={selectedElement}
|
||||
value={freeformInput}
|
||||
unvalidatedText={freeformInputUnvalidated}
|
||||
|
@ -464,7 +462,6 @@
|
|||
{feedback}
|
||||
{unit}
|
||||
{state}
|
||||
{extraTags}
|
||||
feature={selectedElement}
|
||||
value={freeformInput}
|
||||
unvalidatedText={freeformInputUnvalidated}
|
||||
|
|
207
src/UI/Studio/CollapsedTagRenderingPreview.svelte
Normal file
207
src/UI/Studio/CollapsedTagRenderingPreview.svelte
Normal file
|
@ -0,0 +1,207 @@
|
|||
<script lang="ts">
|
||||
|
||||
import Translations from "../i18n/Translations"
|
||||
import type { ConfigMeta } from "./configMeta"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import SchemaBasedInput from "./SchemaBasedInput.svelte"
|
||||
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import QuestionPreview from "./QuestionPreview.svelte"
|
||||
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"
|
||||
import { EditJsonState } from "./EditLayerState"
|
||||
import type {
|
||||
QuestionableTagRenderingConfigJson,
|
||||
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
import { AccordionItem } from "flowbite-svelte"
|
||||
|
||||
export let state: EditJsonState<any>
|
||||
|
||||
export let isTagRenderingBlock: boolean
|
||||
export let schema: ConfigMeta
|
||||
export let currentValue: UIEventSource<(string | QuestionableTagRenderingConfigJson)[]>
|
||||
|
||||
export let value: any
|
||||
export let singular: string
|
||||
export let i: number
|
||||
export let path: (string | number)[] = []
|
||||
|
||||
export let expanded = new UIEventSource(false)
|
||||
|
||||
const subparts: ConfigMeta[] = state
|
||||
.getSchemaStartingWith(schema.path)
|
||||
.filter((part) => part.path.length - 1 === schema.path.length)
|
||||
|
||||
function schemaForMultitype() {
|
||||
const sch = { ...schema }
|
||||
sch.hints.typehint = undefined
|
||||
return sch
|
||||
}
|
||||
|
||||
function fusePath(i: number, subpartPath: string[]): (string | number)[] {
|
||||
const newPath = [...path, i]
|
||||
const toAdd = [...subpartPath]
|
||||
for (const part of path) {
|
||||
if (toAdd[0] === part) {
|
||||
toAdd.splice(0, 1)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
newPath.push(...toAdd)
|
||||
return newPath
|
||||
}
|
||||
|
||||
function del(i: number) {
|
||||
expanded.setData(false)
|
||||
currentValue.data.splice(i, 1)
|
||||
currentValue.ping()
|
||||
}
|
||||
|
||||
function swap(i: number, j: number) {
|
||||
expanded.setData(false)
|
||||
const x = currentValue.data[i]
|
||||
currentValue.data[i] = currentValue.data[j]
|
||||
currentValue.data[j] = x
|
||||
currentValue.ping()
|
||||
}
|
||||
|
||||
function moveTo(source: number, target: number) {
|
||||
expanded.setData(false)
|
||||
const x = currentValue.data[source]
|
||||
currentValue.data.splice(source, 1)
|
||||
currentValue.data.splice(target, 0, x)
|
||||
currentValue.ping()
|
||||
}
|
||||
|
||||
function genTitle(value: any, singular: string, i: number): Translation {
|
||||
try {
|
||||
if (schema.hints.title) {
|
||||
const v = Function("value", "return " + schema.hints.title)(value)
|
||||
return Translations.T(v)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"Warning: could not translate a title for " +
|
||||
`${singular} ${i} with function ` +
|
||||
schema.hints.title +
|
||||
" and value " +
|
||||
JSON.stringify(value),
|
||||
)
|
||||
}
|
||||
return Translations.T(`${singular} ${i}`)
|
||||
}
|
||||
|
||||
let genIconF: (x: any) => { icon: string; color: string } = <any>(
|
||||
Function("value", "return " + schema.hints.icon)
|
||||
)
|
||||
|
||||
function genIcon(value: any): string {
|
||||
return genIconF(value)?.icon
|
||||
}
|
||||
|
||||
function genColor(value: any): string {
|
||||
if (!schema.hints.icon) {
|
||||
return undefined
|
||||
}
|
||||
return genIconF(value)?.color
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<AccordionItem open={$expanded} paddingDefault="p-0" inactiveClass="text-black m-0" >
|
||||
<div slot="header" class="p-1 text-base w-full m-0 text-black">
|
||||
{#if !isTagRenderingBlock}
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="m-0 flex">
|
||||
{#if schema.hints.icon}
|
||||
<Icon clss="w-6 h-6" icon={genIcon(value)} color={genColor(value)} />
|
||||
{/if}
|
||||
{#if schema.hints.title}
|
||||
<Tr t={genTitle(value, singular, i)} />
|
||||
<div class="subtle ml-2">
|
||||
{singular}
|
||||
{i}
|
||||
</div>
|
||||
{:else}
|
||||
{singular}
|
||||
{i}
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
class="h-fit w-fit rounded-full border border-black p-1"
|
||||
on:click={() => {
|
||||
del(i)
|
||||
}}
|
||||
>
|
||||
<TrashIcon class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
{:else if typeof value === "string"}
|
||||
Builtin: <b>{value}</b>
|
||||
{:else if value["builtin"]}
|
||||
reused tagrendering <span class="font-bold">{JSON.stringify(value["builtin"])}</span>
|
||||
{:else}
|
||||
<Tr cls="font-bold" t={Translations.T(value?.question ?? value?.render)} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="normal-background p-2">
|
||||
{#if isTagRenderingBlock}
|
||||
<QuestionPreview {state} {path} {schema}>
|
||||
<button
|
||||
on:click={() => {
|
||||
del(i)
|
||||
}}
|
||||
>
|
||||
<TrashIcon class="h-4 w-4" />
|
||||
Delete this question
|
||||
</button>
|
||||
|
||||
{#if i > 0}
|
||||
<button
|
||||
on:click={() => {
|
||||
moveTo(i, 0)
|
||||
}}
|
||||
>
|
||||
Move to front
|
||||
</button>
|
||||
|
||||
<button
|
||||
on:click={() => {
|
||||
swap(i, i - 1)
|
||||
}}
|
||||
>
|
||||
Move up
|
||||
</button>
|
||||
{/if}
|
||||
{#if i + 1 < $currentValue.length}
|
||||
<button
|
||||
on:click={() => {
|
||||
swap(i, i + 1)
|
||||
}}
|
||||
>
|
||||
Move down
|
||||
</button>
|
||||
<button
|
||||
on:click={() => {
|
||||
moveTo(i, $currentValue.length - 1)
|
||||
}}
|
||||
>
|
||||
Move to back
|
||||
</button>
|
||||
{/if}
|
||||
</QuestionPreview>
|
||||
{:else if schema.hints.types}
|
||||
<SchemaBasedMultiType {state} path={fusePath(i, [])} schema={schemaForMultitype()} />
|
||||
{:else}
|
||||
{#each subparts as subpart}
|
||||
<SchemaBasedInput
|
||||
{state}
|
||||
path={fusePath(i, [subpart.path.at(-1)])}
|
||||
schema={subpart}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</AccordionItem>
|
|
@ -8,7 +8,7 @@ import {
|
|||
Pipe,
|
||||
} from "../../Models/ThemeConfig/Conversion/Conversion"
|
||||
import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer"
|
||||
import { ValidateLayer, ValidateTheme } from "../../Models/ThemeConfig/Conversion/Validation"
|
||||
import { PrevalidateTheme, ValidateLayer, ValidateTheme } from "../../Models/ThemeConfig/Conversion/Validation"
|
||||
import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
|
||||
import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
|
@ -33,6 +33,8 @@ export abstract class EditJsonState<T> {
|
|||
public readonly schema: ConfigMeta[]
|
||||
public readonly category: "layers" | "themes"
|
||||
public readonly server: StudioServer
|
||||
public readonly osmConnection: OsmConnection
|
||||
|
||||
public readonly showIntro: UIEventSource<"no" | "intro" | "tagrenderings"> = <any>(
|
||||
LocalStorageSource.Get("studio-show-intro", "intro")
|
||||
)
|
||||
|
@ -51,7 +53,7 @@ export abstract class EditJsonState<T> {
|
|||
* The EditLayerUI shows a 'schemaBasedInput' for this path to pop advanced questions out
|
||||
*/
|
||||
public readonly highlightedItem: UIEventSource<HighlightedTagRendering> = new UIEventSource(
|
||||
undefined
|
||||
undefined,
|
||||
)
|
||||
private sendingUpdates = false
|
||||
private readonly _stores = new Map<string, UIEventSource<any>>()
|
||||
|
@ -60,10 +62,12 @@ export abstract class EditJsonState<T> {
|
|||
schema: ConfigMeta[],
|
||||
server: StudioServer,
|
||||
category: "layers" | "themes",
|
||||
osmConnection: OsmConnection,
|
||||
options?: {
|
||||
expertMode?: UIEventSource<boolean>
|
||||
}
|
||||
},
|
||||
) {
|
||||
this.osmConnection = osmConnection
|
||||
this.schema = schema
|
||||
this.server = server
|
||||
this.category = category
|
||||
|
@ -88,6 +92,10 @@ export abstract class EditJsonState<T> {
|
|||
await this.server.update(id, config, this.category)
|
||||
})
|
||||
this.messages = this.createMessagesStore()
|
||||
this.register(["credits"], this.osmConnection.userDetails.mapD(u => u.name), false)
|
||||
this.register(["credits:uid"], this.osmConnection.userDetails.mapD(u => u.uid), false)
|
||||
|
||||
|
||||
}
|
||||
|
||||
public startSavingUpdates(enabled = true) {
|
||||
|
@ -132,7 +140,7 @@ export abstract class EditJsonState<T> {
|
|||
public register(
|
||||
path: ReadonlyArray<string | number>,
|
||||
value: Store<any>,
|
||||
noInitialSync: boolean = true
|
||||
noInitialSync: boolean = true,
|
||||
): () => void {
|
||||
const unsync = value.addCallback((v) => {
|
||||
this.setValueAt(path, v)
|
||||
|
@ -146,7 +154,7 @@ export abstract class EditJsonState<T> {
|
|||
public getSchemaStartingWith(path: string[]) {
|
||||
return this.schema.filter(
|
||||
(sch) =>
|
||||
!path.some((part, i) => !(sch.path.length > path.length && sch.path[i] === part))
|
||||
!path.some((part, i) => !(sch.path.length > path.length && sch.path[i] === part)),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -167,7 +175,7 @@ export abstract class EditJsonState<T> {
|
|||
const schemas = this.schema.filter(
|
||||
(sch) =>
|
||||
sch !== undefined &&
|
||||
!path.some((part, i) => !(sch.path.length == path.length && sch.path[i] === part))
|
||||
!path.some((part, i) => !(sch.path.length == path.length && sch.path[i] === part)),
|
||||
)
|
||||
if (schemas.length == 0) {
|
||||
console.warn("No schemas found for path", path.join("."))
|
||||
|
@ -257,12 +265,12 @@ class ContextRewritingStep<T> extends Conversion<LayerConfigJson, T> {
|
|||
constructor(
|
||||
state: DesugaringContext,
|
||||
step: Conversion<LayerConfigJson, T>,
|
||||
getTagRenderings: (t: T) => TagRenderingConfigJson[]
|
||||
getTagRenderings: (t: T) => TagRenderingConfigJson[],
|
||||
) {
|
||||
super(
|
||||
"When validating a layer, the tagRenderings are first expanded. Some builtin tagRendering-calls (e.g. `contact`) will introduce _multiple_ tagRenderings, causing the count to be off. This class rewrites the error messages to fix this",
|
||||
[],
|
||||
"ContextRewritingStep"
|
||||
"ContextRewritingStep",
|
||||
)
|
||||
this._state = state
|
||||
this._step = step
|
||||
|
@ -272,7 +280,7 @@ class ContextRewritingStep<T> extends Conversion<LayerConfigJson, T> {
|
|||
convert(json: LayerConfigJson, context: ConversionContext): T {
|
||||
const converted = this._step.convert(json, context)
|
||||
const originalIds = json.tagRenderings?.map(
|
||||
(tr) => (<QuestionableTagRenderingConfigJson>tr)["id"]
|
||||
(tr) => (<QuestionableTagRenderingConfigJson>tr)["id"],
|
||||
)
|
||||
if (!originalIds) {
|
||||
return converted
|
||||
|
@ -307,7 +315,6 @@ class ContextRewritingStep<T> extends Conversion<LayerConfigJson, T> {
|
|||
|
||||
export default class EditLayerState extends EditJsonState<LayerConfigJson> {
|
||||
// Needed for the special visualisations
|
||||
public readonly osmConnection: OsmConnection
|
||||
public readonly imageUploadManager = {
|
||||
getCountsFor() {
|
||||
return 0
|
||||
|
@ -335,10 +342,9 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
|
|||
schema: ConfigMeta[],
|
||||
server: StudioServer,
|
||||
osmConnection: OsmConnection,
|
||||
options: { expertMode: UIEventSource<boolean> }
|
||||
options: { expertMode: UIEventSource<boolean> },
|
||||
) {
|
||||
super(schema, server, "layers", options)
|
||||
this.osmConnection = osmConnection
|
||||
super(schema, server, "layers", osmConnection, options)
|
||||
this.layout = {
|
||||
getMatchingLayer: () => {
|
||||
try {
|
||||
|
@ -393,7 +399,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
|
|||
return new ContextRewritingStep(
|
||||
state,
|
||||
new Pipe(new PrepareLayer(state), new ValidateLayer("dynamic", false, undefined, true)),
|
||||
(t) => <TagRenderingConfigJson[]>t.raw.tagRenderings
|
||||
(t) => <TagRenderingConfigJson[]>t.raw.tagRenderings,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -427,7 +433,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
|
|||
}
|
||||
|
||||
protected async validate(
|
||||
configuration: Partial<LayerConfigJson>
|
||||
configuration: Partial<LayerConfigJson>,
|
||||
): Promise<ConversionMessage[]> {
|
||||
const layers = AllSharedLayers.getSharedLayersConfigs()
|
||||
|
||||
|
@ -456,16 +462,19 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> {
|
|||
constructor(
|
||||
schema: ConfigMeta[],
|
||||
server: StudioServer,
|
||||
options: { expertMode: UIEventSource<boolean> }
|
||||
osmConnection: OsmConnection,
|
||||
options: { expertMode: UIEventSource<boolean> },
|
||||
) {
|
||||
super(schema, server, "themes", options)
|
||||
super(schema, server, "themes", osmConnection, options)
|
||||
this.setupFixers()
|
||||
}
|
||||
|
||||
protected buildValidation(state: DesugaringContext): Conversion<LayoutConfigJson, any> {
|
||||
return new Pipe(
|
||||
return new Pipe(new PrevalidateTheme(),
|
||||
new Pipe(
|
||||
new PrepareTheme(state),
|
||||
new ValidateTheme(undefined, "", false, new Set(state.tagRenderings.keys()))
|
||||
new ValidateTheme(undefined, "", false, new Set(state.tagRenderings.keys())),
|
||||
), true,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type { ConfigMeta } from "./configMeta"
|
||||
import EditLayerState from "./EditLayerState"
|
||||
import EditLayerState, { EditJsonState } from "./EditLayerState"
|
||||
import * as questions from "../../assets/generated/layers/questions.json"
|
||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js"
|
||||
import type {
|
||||
QuestionableTagRenderingConfigJson,
|
||||
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js"
|
||||
import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
|
||||
import FromHtml from "../Base/FromHtml.svelte"
|
||||
import ShowConversionMessage from "./ShowConversionMessage.svelte"
|
||||
|
@ -31,15 +33,25 @@
|
|||
if (typeof x === "string") {
|
||||
return perId[x]
|
||||
} else {
|
||||
return [x]
|
||||
return <any>[x]
|
||||
}
|
||||
})
|
||||
let configs: Store<TagRenderingConfig[]> = configJson.map((configs) => {
|
||||
if (!configs) {
|
||||
return [{ error: "No configuartions found" }]
|
||||
}
|
||||
console.log("Regenerating configs")
|
||||
return configs.map((config) => {
|
||||
if (config["builtin"]) {
|
||||
let override = ""
|
||||
if (config["override"]) {
|
||||
override = ". Some items are changed with an override. Editing this is not yet supported with Studio."
|
||||
}
|
||||
return new TagRenderingConfig({
|
||||
render: {
|
||||
"en": "This question reuses <b>" + JSON.stringify(config["builtin"]) + "</b>" + override,
|
||||
},
|
||||
})
|
||||
}
|
||||
try {
|
||||
return new TagRenderingConfig(config)
|
||||
} catch (e) {
|
||||
|
@ -47,15 +59,6 @@
|
|||
}
|
||||
})
|
||||
})
|
||||
let id: Store<string> = value.mapD((c) => {
|
||||
if (c?.id) {
|
||||
return c.id
|
||||
}
|
||||
if (typeof c === "string") {
|
||||
return c
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
let tags = state.testTags
|
||||
|
||||
|
@ -66,11 +69,18 @@
|
|||
|
||||
<div class="flex">
|
||||
<div class="m-4 flex w-full flex-col">
|
||||
{#if $configJson.some(config => config["builtin"] !== undefined)}
|
||||
<div class="interactive p-2 rounded-2xl">
|
||||
This question uses an advanced 'builtin'+'override' construction in the source code.
|
||||
Editing this with Studio is not supported.
|
||||
</div>
|
||||
{:else}
|
||||
<NextButton clss="primary" on:click={() => state.highlightedItem.setData({ path, schema })}>
|
||||
{#if schema.hints.question}
|
||||
{schema.hints.question}
|
||||
{/if}
|
||||
</NextButton>
|
||||
{/if}
|
||||
{#if description}
|
||||
<Markdown src={description} />
|
||||
{/if}
|
||||
|
@ -86,6 +96,7 @@
|
|||
{#each $configs as config}
|
||||
{#if config.error !== undefined}
|
||||
<div class="alert">Could not create a preview of this tagRendering: {config.error}</div>
|
||||
{JSON.stringify($value)}
|
||||
{:else if config.condition && !config.condition.matchesProperties($tags)}
|
||||
This tagRendering is currently not shown. It will appear if the feature matches the
|
||||
condition
|
||||
|
|
|
@ -8,6 +8,15 @@
|
|||
|
||||
export let state: EditLayerState | EditThemeState
|
||||
|
||||
let rawConfig = state.configuration.sync(f => JSON.stringify(f, null, " "), [], json => {
|
||||
try {
|
||||
return JSON.parse(json)
|
||||
} catch (e) {
|
||||
console.error("Could not parse", json)
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
|
||||
let container: HTMLDivElement
|
||||
let monaco: typeof Monaco
|
||||
let editor: Monaco.editor.IStandaloneCodeEditor
|
||||
|
@ -34,13 +43,19 @@
|
|||
return () => window.removeEventListener("keydown", handler)
|
||||
})
|
||||
|
||||
let useFallback = false
|
||||
onMount(async () => {
|
||||
const monacoEditor = await import("monaco-editor")
|
||||
loader.config({
|
||||
monaco: monacoEditor.default,
|
||||
})
|
||||
|
||||
try {
|
||||
monaco = await loader.init()
|
||||
} catch (e) {
|
||||
console.error("Could not load Monaco Editor, falling back", e)
|
||||
useFallback = true
|
||||
}
|
||||
|
||||
// Determine schema based on the state
|
||||
let schemaUri: string
|
||||
|
@ -50,7 +65,7 @@
|
|||
schemaUri = "https://mapcomplete.org/schemas/layoutconfig.json"
|
||||
}
|
||||
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
monaco?.languages?.json?.jsonDefaults?.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
schemas: [
|
||||
{
|
||||
|
@ -64,23 +79,28 @@
|
|||
],
|
||||
})
|
||||
|
||||
let modelUri = monaco.Uri.parse("inmemory://inmemory/file.json")
|
||||
let modelUri = monaco?.Uri?.parse("inmemory://inmemory/file.json")
|
||||
|
||||
// Create a new model
|
||||
model = monaco.editor.createModel(
|
||||
try {
|
||||
model = monaco?.editor?.createModel(
|
||||
JSON.stringify(state.configuration.data, null, " "),
|
||||
"json",
|
||||
modelUri
|
||||
modelUri,
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("Could not create model in MOnaco Editor", e)
|
||||
useFallback = true
|
||||
}
|
||||
|
||||
editor = monaco.editor.create(container, {
|
||||
editor = monaco?.editor?.create(container, {
|
||||
model,
|
||||
automaticLayout: true,
|
||||
})
|
||||
|
||||
// When the editor is changed, update the configuration, but only if the user hasn't typed for 500ms and the JSON is valid
|
||||
let timeout: number
|
||||
editor.onDidChangeModelContent(() => {
|
||||
editor?.onDidChangeModelContent(() => {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
save()
|
||||
|
@ -98,4 +118,8 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class="h-full w-full" />
|
||||
{#if useFallback}
|
||||
<textarea class="w-full" rows="25" bind:value={$rawConfig} />
|
||||
{:else}
|
||||
<div bind:this={container} class="h-full w-full" />
|
||||
{/if}
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
* They will typically be a subset of some properties
|
||||
*/
|
||||
import type { ConfigMeta } from "./configMeta"
|
||||
import EditLayerState from "./EditLayerState"
|
||||
import EditLayerState, { EditJsonState } from "./EditLayerState"
|
||||
import SchemaBasedInput from "./SchemaBasedInput.svelte"
|
||||
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export let state: EditLayerState
|
||||
export let configs: ConfigMeta[]
|
||||
|
@ -30,8 +31,8 @@
|
|||
<div slot="header">{title}</div>
|
||||
<div class="flex w-full flex-col gap-y-1 pl-2">
|
||||
<slot name="description" />
|
||||
{#each configsFiltered as config}
|
||||
<SchemaBasedInput {state} path={config.path} schema={config} />
|
||||
{#each configsFiltered as config (config.path)}
|
||||
<SchemaBasedInput {state} path={config.path} } />
|
||||
{/each}
|
||||
</div>
|
||||
</AccordionSingle>
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
<script lang="ts">
|
||||
import EditLayerState from "./EditLayerState"
|
||||
import { EditJsonState } from "./EditLayerState"
|
||||
import type { ConfigMeta } from "./configMeta"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import SchemaBasedInput from "./SchemaBasedInput.svelte"
|
||||
import SchemaBasedField from "./SchemaBasedField.svelte"
|
||||
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import QuestionPreview from "./QuestionPreview.svelte"
|
||||
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"
|
||||
import ShowConversionMessage from "./ShowConversionMessage.svelte"
|
||||
import Markdown from "../Base/Markdown.svelte"
|
||||
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Utils } from "../../Utils"
|
||||
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
import CollapsedTagRenderingPreview from "./CollapsedTagRenderingPreview.svelte"
|
||||
import { Accordion } from "flowbite-svelte"
|
||||
|
||||
export let state: EditLayerState
|
||||
export let state: EditJsonState<any>
|
||||
export let path: (string | number)[] = []
|
||||
export let schema: ConfigMeta
|
||||
|
||||
schema = Utils.Clone(schema)
|
||||
let title = schema.path.at(-1)
|
||||
console.log(">>>", schema)
|
||||
let singular = title
|
||||
if (title?.endsWith("s")) {
|
||||
singular = title.slice(0, title.length - 1)
|
||||
|
@ -27,7 +24,6 @@
|
|||
if (singular?.match(/^[aeoui]/)) {
|
||||
article = "an"
|
||||
}
|
||||
export let path: (string | number)[] = []
|
||||
|
||||
const isTagRenderingBlock = path.length === 1 && path[0] === "tagRenderings"
|
||||
|
||||
|
@ -41,12 +37,12 @@
|
|||
.filter((part) => part.path.length - 1 === schema.path.length)
|
||||
let messages = state.messagesFor(path)
|
||||
|
||||
const currentValue: UIEventSource<any[]> = state.getStoreFor(path)
|
||||
const currentValue = state.getStoreFor<(string | QuestionableTagRenderingConfigJson)[]>(path)
|
||||
if (currentValue.data === undefined) {
|
||||
currentValue.setData([])
|
||||
}
|
||||
|
||||
function createItem(valueToSet?: any) {
|
||||
function createItem(valueToSet?: string | QuestionableTagRenderingConfigJson) {
|
||||
if (currentValue.data === undefined) {
|
||||
currentValue.setData([])
|
||||
}
|
||||
|
@ -72,64 +68,13 @@
|
|||
return newPath
|
||||
}
|
||||
|
||||
function schemaForMultitype() {
|
||||
const sch = { ...schema }
|
||||
sch.hints.typehint = undefined
|
||||
return sch
|
||||
}
|
||||
|
||||
function del(i: number) {
|
||||
currentValue.data.splice(i, 1)
|
||||
currentValue.ping()
|
||||
}
|
||||
|
||||
function swap(i: number, j: number) {
|
||||
const x = currentValue.data[i]
|
||||
currentValue.data[i] = currentValue.data[j]
|
||||
currentValue.data[j] = x
|
||||
currentValue.ping()
|
||||
}
|
||||
|
||||
function moveTo(source: number, target: number) {
|
||||
const x = currentValue.data[source]
|
||||
currentValue.data.splice(source, 1)
|
||||
currentValue.data.splice(target, 0, x)
|
||||
currentValue.ping()
|
||||
}
|
||||
|
||||
function genTitle(value: any, singular: string, i: number): Translation {
|
||||
try {
|
||||
if (schema.hints.title) {
|
||||
const v = Function("value", "return " + schema.hints.title)(value)
|
||||
return Translations.T(v)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"Warning: could not translate a title for " +
|
||||
`${singular} ${i} with function ` +
|
||||
schema.hints.title +
|
||||
" and value " +
|
||||
JSON.stringify(value)
|
||||
)
|
||||
}
|
||||
return Translations.T(`${singular} ${i}`)
|
||||
}
|
||||
|
||||
let genIconF: (x: any) => { icon: string; color: string } = <any>(
|
||||
Function("value", "return " + schema.hints.icon)
|
||||
)
|
||||
console.log("Icon lambda is", schema.hints.icon, path, genIconF("test"))
|
||||
|
||||
function genIcon(value: any): string {
|
||||
return genIconF(value)?.icon
|
||||
}
|
||||
|
||||
function genColor(value: any): string {
|
||||
if (!schema.hints.icon) {
|
||||
return undefined
|
||||
}
|
||||
return genIconF(value)?.color
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="pl-2">
|
||||
|
@ -163,99 +108,11 @@
|
|||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each $currentValue as value, i}
|
||||
<AccordionSingle expanded={false}>
|
||||
<span slot="header">
|
||||
{#if !isTagRenderingBlock}
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="m-0 flex">
|
||||
{#if schema.hints.icon}
|
||||
<Icon clss="w-6 h-6" icon={genIcon(value)} color={genColor(value)} />
|
||||
{/if}
|
||||
{singular}
|
||||
{i}
|
||||
|
||||
{#if schema.hints.title}
|
||||
<div class="subtle ml-2">
|
||||
<Tr t={genTitle(value, singular, i)} />
|
||||
</div>
|
||||
{/if}
|
||||
</h3>
|
||||
<button
|
||||
class="h-fit w-fit rounded-full border border-black p-1"
|
||||
on:click={() => {
|
||||
del(i)
|
||||
}}
|
||||
>
|
||||
<TrashIcon class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
{:else if typeof value === "string"}
|
||||
Builtin: <b>{value}</b>
|
||||
{:else}
|
||||
<Tr cls="font-bold" t={Translations.T(value?.question ?? value?.render)} />
|
||||
{/if}
|
||||
</span>
|
||||
<div class="normal-background p-2">
|
||||
{#if isTagRenderingBlock}
|
||||
<QuestionPreview {state} path={fusePath(i, [])} {schema}>
|
||||
<button
|
||||
on:click={() => {
|
||||
del(i)
|
||||
}}
|
||||
>
|
||||
<TrashIcon class="h-4 w-4" />
|
||||
Delete this question
|
||||
</button>
|
||||
|
||||
{#if i > 0}
|
||||
<button
|
||||
on:click={() => {
|
||||
moveTo(i, 0)
|
||||
}}
|
||||
>
|
||||
Move to front
|
||||
</button>
|
||||
|
||||
<button
|
||||
on:click={() => {
|
||||
swap(i, i - 1)
|
||||
}}
|
||||
>
|
||||
Move up
|
||||
</button>
|
||||
{/if}
|
||||
{#if i + 1 < $currentValue.length}
|
||||
<button
|
||||
on:click={() => {
|
||||
swap(i, i + 1)
|
||||
}}
|
||||
>
|
||||
Move down
|
||||
</button>
|
||||
<button
|
||||
on:click={() => {
|
||||
moveTo(i, $currentValue.length - 1)
|
||||
}}
|
||||
>
|
||||
Move to back
|
||||
</button>
|
||||
{/if}
|
||||
</QuestionPreview>
|
||||
{:else if schema.hints.types}
|
||||
<SchemaBasedMultiType {state} path={fusePath(i, [])} schema={schemaForMultitype()} />
|
||||
{:else}
|
||||
{#each subparts as subpart}
|
||||
<SchemaBasedInput
|
||||
{state}
|
||||
path={fusePath(i, [subpart.path.at(-1)])}
|
||||
schema={subpart}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</AccordionSingle>
|
||||
<Accordion>
|
||||
{#each $currentValue as value, i (value)}
|
||||
<CollapsedTagRenderingPreview {state} {isTagRenderingBlock} {schema} {currentValue} {value} {i} {singular} path={fusePath(i, [])}/>
|
||||
{/each}
|
||||
</Accordion>
|
||||
{/if}
|
||||
<div class="flex">
|
||||
<button on:click={() => createItem()}>Add {article} {singular}</button>
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
import EditLayerState from "./EditLayerState"
|
||||
import { EditJsonState } from "./EditLayerState"
|
||||
import { onDestroy } from "svelte"
|
||||
import type { JsonSchemaType } from "./jsonSchema"
|
||||
import { ConfigMetaUtils } from "./configMeta"
|
||||
import ShowConversionMessage from "./ShowConversionMessage.svelte"
|
||||
|
||||
export let state: EditLayerState
|
||||
export let state: EditJsonState<any>
|
||||
export let path: (string | number)[] = []
|
||||
export let schema: ConfigMeta
|
||||
export let startInEditModeIfUnset: boolean = schema.hints && !schema.hints.ifunset
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
<script lang="ts">
|
||||
import type { ConfigMeta } from "./configMeta"
|
||||
import SchemaBasedField from "./SchemaBasedField.svelte"
|
||||
import EditLayerState from "./EditLayerState"
|
||||
import { EditJsonState } from "./EditLayerState"
|
||||
import SchemaBasedArray from "./SchemaBasedArray.svelte"
|
||||
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"
|
||||
import ArrayMultiAnswer from "./ArrayMultiAnswer.svelte"
|
||||
|
||||
export let schema: ConfigMeta
|
||||
export let state: EditLayerState
|
||||
export let state: EditJsonState<any>
|
||||
export let path: (string | number)[] = []
|
||||
console.log("Fetched schema:", path, state.getSchema(<any> path))
|
||||
let schema: ConfigMeta = state.getSchema(<any> path)[0]
|
||||
let expertMode = state.expertMode
|
||||
</script>
|
||||
|
||||
|
|
|
@ -204,9 +204,10 @@
|
|||
</script>
|
||||
|
||||
<div class="m-1 flex flex-col gap-y-2 border-2 border-dashed border-gray-300 p-2">
|
||||
{#if schema.hints.title !== undefined}
|
||||
{#if schema.hints.title !== undefined && typeof path.at(-1) !== "number"}
|
||||
<h3>{schema.hints.title}</h3>
|
||||
<div>{schema.description}</div>
|
||||
{path.join(".")}
|
||||
{/if}
|
||||
{#if hasOverride}
|
||||
This object refers to {existingValue.builtin} and overrides some properties. This cannot be edited
|
||||
|
@ -221,7 +222,6 @@
|
|||
{#if $expertMode || subschema.hints?.group !== "expert"}
|
||||
<SchemaBasedInput
|
||||
{state}
|
||||
schema={subschema}
|
||||
path={[...subpath, subschema?.path?.at(-1) ?? "???"]}
|
||||
/>
|
||||
{:else if window.location.hostname === "127.0.0.1"}
|
||||
|
|
|
@ -42,9 +42,11 @@
|
|||
undefined,
|
||||
"Used to complete the login"
|
||||
)
|
||||
const fakeUser = UIEventSource.asBoolean( QueryParameters.GetQueryParameter("fake-user", "Test switch for fake login"))
|
||||
let osmConnection = new OsmConnection({
|
||||
oauth_token,
|
||||
checkOnlineRegularly: true,
|
||||
fakeUser: fakeUser.data
|
||||
})
|
||||
const expertMode = UIEventSource.asBoolean(
|
||||
osmConnection.GetPreference("studio-expert-mode", "false", {
|
||||
|
@ -129,7 +131,7 @@
|
|||
let showIntro = editLayerState.showIntro
|
||||
|
||||
const layoutSchema: ConfigMeta[] = <any>layoutSchemaRaw
|
||||
let editThemeState = new EditThemeState(layoutSchema, studio, { expertMode })
|
||||
let editThemeState = new EditThemeState(layoutSchema, studio, osmConnection, { expertMode })
|
||||
|
||||
const version = meta.version
|
||||
|
||||
|
|
|
@ -335,6 +335,17 @@ textarea {
|
|||
color: black;
|
||||
}
|
||||
|
||||
h2.group {
|
||||
/* For flowbite accordions */
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.group button {
|
||||
/* For flowbite accordions */
|
||||
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/************************* OTHER CATEGORIES ********************************/
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue