Studio: improvements after user test
This commit is contained in:
parent
449c1adb00
commit
e79a0fc81d
59 changed files with 1312 additions and 2920 deletions
|
@ -1,6 +1,7 @@
|
|||
import { ConversionContext, DesugaringStep } from "./Conversion"
|
||||
import { DesugaringStep } from "./Conversion"
|
||||
import { Utils } from "../../../Utils"
|
||||
import Translations from "../../../UI/i18n/Translations"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
export class AddContextToTranslations<T> extends DesugaringStep<T> {
|
||||
private readonly _prefix: string
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
export interface DesugaringContext {
|
||||
tagRenderings: Map<string, QuestionableTagRenderingConfigJson>
|
||||
|
@ -8,112 +9,6 @@ export interface DesugaringContext {
|
|||
publicLayers?: Set<string>
|
||||
}
|
||||
|
||||
export class ConversionContext {
|
||||
/**
|
||||
* The path within the data structure where we are currently operating
|
||||
*/
|
||||
readonly path: ReadonlyArray<string | number>
|
||||
/**
|
||||
* Some information about the current operation
|
||||
*/
|
||||
readonly operation: ReadonlyArray<string>
|
||||
readonly messages: ConversionMessage[]
|
||||
|
||||
private constructor(
|
||||
messages: ConversionMessage[],
|
||||
path: ReadonlyArray<string | number>,
|
||||
operation?: ReadonlyArray<string>
|
||||
) {
|
||||
this.path = path
|
||||
this.operation = operation ?? []
|
||||
// Messages is shared by reference amonst all 'context'-objects for performance
|
||||
this.messages = messages
|
||||
if (this.path.some((p) => typeof p === "object" || p === "[object Object]")) {
|
||||
throw "ConversionMessage: got an object as path entry:" + JSON.stringify(path)
|
||||
}
|
||||
}
|
||||
|
||||
public static construct(path: (string | number)[], operation: string[]) {
|
||||
return new ConversionContext([], [...path], [...operation])
|
||||
}
|
||||
|
||||
public static test(msg?: string) {
|
||||
return new ConversionContext([], msg ? [msg] : [], ["test"])
|
||||
}
|
||||
|
||||
static print(msg: ConversionMessage) {
|
||||
const noString = msg.context.path.filter(
|
||||
(p) => typeof p !== "string" && typeof p !== "number"
|
||||
)
|
||||
if (noString.length > 0) {
|
||||
console.warn("Non-string value in path:", ...noString)
|
||||
}
|
||||
if (msg.level === "error") {
|
||||
console.error(
|
||||
ConversionContext.red("ERR "),
|
||||
msg.context.path.join("."),
|
||||
ConversionContext.red(msg.message),
|
||||
msg.context.operation.join(".")
|
||||
)
|
||||
} else if (msg.level === "warning") {
|
||||
console.warn(
|
||||
ConversionContext.red("<!> "),
|
||||
msg.context.path.join("."),
|
||||
ConversionContext.yellow(msg.message),
|
||||
msg.context.operation.join(".")
|
||||
)
|
||||
} else {
|
||||
console.log(" ", msg.context.path.join("."), msg.message)
|
||||
}
|
||||
}
|
||||
|
||||
private static yellow(s) {
|
||||
return "\x1b[33m" + s + "\x1b[0m"
|
||||
}
|
||||
|
||||
private static red(s) {
|
||||
return "\x1b[31m" + s + "\x1b[0m"
|
||||
}
|
||||
|
||||
public enter(key: string | number | (string | number)[]) {
|
||||
if (!Array.isArray(key)) {
|
||||
return new ConversionContext(this.messages, [...this.path, key], this.operation)
|
||||
}
|
||||
return new ConversionContext(this.messages, [...this.path, ...key], this.operation)
|
||||
}
|
||||
|
||||
public enters(...key: (string | number)[]) {
|
||||
return this.enter(key)
|
||||
}
|
||||
|
||||
public inOperation(key: string) {
|
||||
return new ConversionContext(this.messages, this.path, [...this.operation, key])
|
||||
}
|
||||
|
||||
warn(message: string) {
|
||||
this.messages.push({ context: this, level: "warning", message })
|
||||
}
|
||||
|
||||
err(message: string) {
|
||||
this.messages.push({ context: this, level: "error", message })
|
||||
}
|
||||
|
||||
info(message: string) {
|
||||
this.messages.push({ context: this, level: "information", message })
|
||||
}
|
||||
|
||||
getAll(mode: ConversionMsgLevel): ConversionMessage[] {
|
||||
return this.messages.filter((m) => m.level === mode)
|
||||
}
|
||||
public hasErrors() {
|
||||
return this.messages?.find((m) => m.level === "error") !== undefined
|
||||
}
|
||||
|
||||
debug(message: string) {
|
||||
this.messages.push({ context: this, level: "debug", message })
|
||||
}
|
||||
}
|
||||
|
||||
export type ConversionMsgLevel = "debug" | "information" | "warning" | "error"
|
||||
export interface ConversionMessage {
|
||||
context: ConversionContext
|
||||
|
|
116
src/Models/ThemeConfig/Conversion/ConversionContext.ts
Normal file
116
src/Models/ThemeConfig/Conversion/ConversionContext.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
import { ConversionMessage, ConversionMsgLevel } from "./Conversion"
|
||||
|
||||
export class ConversionContext {
|
||||
/**
|
||||
* The path within the data structure where we are currently operating
|
||||
*/
|
||||
readonly path: ReadonlyArray<string | number>
|
||||
/**
|
||||
* Some information about the current operation
|
||||
*/
|
||||
readonly operation: ReadonlyArray<string>
|
||||
readonly messages: ConversionMessage[]
|
||||
|
||||
private _hasErrors: boolean = false
|
||||
|
||||
private constructor(
|
||||
messages: ConversionMessage[],
|
||||
path: ReadonlyArray<string | number>,
|
||||
operation?: ReadonlyArray<string>
|
||||
) {
|
||||
this.path = path
|
||||
this.operation = operation ?? []
|
||||
// Messages is shared by reference amonst all 'context'-objects for performance
|
||||
this.messages = messages
|
||||
if (this.path.some((p) => typeof p === "object" || p === "[object Object]")) {
|
||||
throw "ConversionMessage: got an object as path entry:" + JSON.stringify(path)
|
||||
}
|
||||
}
|
||||
|
||||
public static construct(path: (string | number)[], operation: string[]) {
|
||||
return new ConversionContext([], [...path], [...operation])
|
||||
}
|
||||
|
||||
public static test(msg?: string) {
|
||||
return new ConversionContext([], msg ? [msg] : [], ["test"])
|
||||
}
|
||||
|
||||
static print(msg: ConversionMessage) {
|
||||
const noString = msg.context.path.filter(
|
||||
(p) => typeof p !== "string" && typeof p !== "number"
|
||||
)
|
||||
if (noString.length > 0) {
|
||||
console.warn("Non-string value in path:", ...noString)
|
||||
}
|
||||
if (msg.level === "error") {
|
||||
console.error(
|
||||
ConversionContext.red("ERR "),
|
||||
msg.context.path.join("."),
|
||||
ConversionContext.red(msg.message),
|
||||
msg.context.operation.join(".")
|
||||
)
|
||||
} else if (msg.level === "warning") {
|
||||
console.warn(
|
||||
ConversionContext.red("<!> "),
|
||||
msg.context.path.join("."),
|
||||
ConversionContext.yellow(msg.message),
|
||||
msg.context.operation.join(".")
|
||||
)
|
||||
} else {
|
||||
console.log(" ", msg.context.path.join("."), msg.message)
|
||||
}
|
||||
}
|
||||
|
||||
private static yellow(s) {
|
||||
return "\x1b[33m" + s + "\x1b[0m"
|
||||
}
|
||||
|
||||
private static red(s) {
|
||||
return "\x1b[31m" + s + "\x1b[0m"
|
||||
}
|
||||
|
||||
public enter(key: string | number | (string | number)[]) {
|
||||
if (!Array.isArray(key)) {
|
||||
return new ConversionContext(this.messages, [...this.path, key], this.operation)
|
||||
}
|
||||
return new ConversionContext(this.messages, [...this.path, ...key], this.operation)
|
||||
}
|
||||
|
||||
public enters(...key: (string | number)[]) {
|
||||
return this.enter(key)
|
||||
}
|
||||
|
||||
public inOperation(key: string) {
|
||||
return new ConversionContext(this.messages, this.path, [...this.operation, key])
|
||||
}
|
||||
|
||||
warn(message: string) {
|
||||
this.messages.push({ context: this, level: "warning", message })
|
||||
}
|
||||
|
||||
err(message: string) {
|
||||
this._hasErrors = true
|
||||
this.messages.push({ context: this, level: "error", message })
|
||||
}
|
||||
|
||||
info(message: string) {
|
||||
this.messages.push({ context: this, level: "information", message })
|
||||
}
|
||||
|
||||
getAll(mode: ConversionMsgLevel): ConversionMessage[] {
|
||||
return this.messages.filter((m) => m.level === mode)
|
||||
}
|
||||
|
||||
public hasErrors() {
|
||||
if (this._hasErrors) {
|
||||
return true
|
||||
}
|
||||
const foundErr = this.messages?.find((m) => m.level === "error") !== undefined
|
||||
this._hasErrors = foundErr
|
||||
return foundErr
|
||||
}
|
||||
|
||||
debug(message: string) {
|
||||
this.messages.push({ context: this, level: "debug", message })
|
||||
}
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import { Conversion, ConversionContext } from "./Conversion"
|
||||
import { Conversion } from "./Conversion"
|
||||
import LayerConfig from "../LayerConfig"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import Translations from "../../../UI/i18n/Translations"
|
||||
import { Translation, TypedTranslation } from "../../../UI/i18n/Translation"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, LayerConfigJson> {
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Conversion, ConversionContext, DesugaringStep } from "./Conversion"
|
||||
import { Conversion, DesugaringStep } from "./Conversion"
|
||||
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import metapaths from "../../../assets/schemas/layoutconfigmeta.json"
|
||||
|
@ -6,6 +6,7 @@ import tagrenderingmetapaths from "../../../assets/schemas/questionabletagrender
|
|||
import Translations from "../../../UI/i18n/Translations"
|
||||
|
||||
import { parse as parse_html } from "node-html-parser"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
export class ExtractImages extends Conversion<
|
||||
LayoutConfigJson,
|
||||
|
|
|
@ -2,8 +2,9 @@ import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
|||
import { Utils } from "../../../Utils"
|
||||
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import { ConversionContext, DesugaringStep, Each, Fuse, On } from "./Conversion"
|
||||
import { DesugaringStep, Each, Fuse, On } from "./Conversion"
|
||||
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
export class UpdateLegacyLayer extends DesugaringStep<
|
||||
LayerConfigJson | string | { builtin; override }
|
||||
|
@ -57,6 +58,9 @@ export class UpdateLegacyLayer extends DesugaringStep<
|
|||
if (config.tagRenderings !== undefined) {
|
||||
let i = 0
|
||||
for (const tagRendering of config.tagRenderings) {
|
||||
if (!tagRendering) {
|
||||
continue
|
||||
}
|
||||
i++
|
||||
if (
|
||||
typeof tagRendering === "string" ||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import {
|
||||
Cached,
|
||||
Concat,
|
||||
Conversion,
|
||||
ConversionContext,
|
||||
DesugaringContext,
|
||||
DesugaringStep,
|
||||
Each,
|
||||
|
@ -32,7 +30,7 @@ import { RenderingSpecification } from "../../../UI/SpecialVisualization"
|
|||
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
||||
import { ConfigMeta } from "../../../UI/Studio/configMeta"
|
||||
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
|
||||
import { j } from "vite-node/types-63205a44"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
||||
private static readonly predefinedFilters = ExpandFilter.load_filters()
|
||||
|
@ -1192,9 +1190,9 @@ class ExpandMarkerRenderings extends DesugaringStep<IconConfigJson> {
|
|||
}
|
||||
}
|
||||
|
||||
export class PrepareLayer extends Cached<LayerConfigJson, LayerConfigJson> {
|
||||
export class PrepareLayer extends Fuse<LayerConfigJson> {
|
||||
constructor(state: DesugaringContext) {
|
||||
const steps = new Fuse<LayerConfigJson>(
|
||||
super(
|
||||
"Fully prepares and expands a layer for the LayerConfig.",
|
||||
new On("tagRenderings", new Each(new RewriteSpecial())),
|
||||
new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
|
||||
|
@ -1224,6 +1222,5 @@ export class PrepareLayer extends Cached<LayerConfigJson, LayerConfigJson> {
|
|||
),
|
||||
new ExpandFilter(state)
|
||||
)
|
||||
super(steps)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
Concat,
|
||||
Conversion,
|
||||
ConversionContext,
|
||||
DesugaringContext,
|
||||
DesugaringStep,
|
||||
Each,
|
||||
|
@ -21,6 +20,7 @@ import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
|||
import DependencyCalculator from "../DependencyCalculator"
|
||||
import { AddContextToTranslations } from "./AddContextToTranslations"
|
||||
import ValidationUtils from "./ValidationUtils"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJson[]> {
|
||||
private readonly _state: DesugaringContext
|
||||
|
|
|
@ -1,13 +1,4 @@
|
|||
import {
|
||||
Conversion,
|
||||
ConversionContext,
|
||||
DesugaringStep,
|
||||
Each,
|
||||
Fuse,
|
||||
On,
|
||||
Pipe,
|
||||
Pure,
|
||||
} from "./Conversion"
|
||||
import { Conversion, DesugaringStep, Each, Fuse, On, Pipe, Pure } from "./Conversion"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import LayerConfig from "../LayerConfig"
|
||||
import { Utils } from "../../../Utils"
|
||||
|
@ -29,6 +20,8 @@ import TagRenderingConfig from "../TagRenderingConfig"
|
|||
import { parse as parse_html } from "node-html-parser"
|
||||
import PresetConfig from "../PresetConfig"
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
|
||||
import { Translatable } from "../Json/Translatable"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
class ValidateLanguageCompleteness extends DesugaringStep<any> {
|
||||
private readonly _languages: string[]
|
||||
|
@ -285,7 +278,7 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
|
|||
new Each(
|
||||
new Pipe(
|
||||
new ValidateLayer(undefined, isBuiltin, doesImageExist, false, true),
|
||||
new Pure((x) => x.raw)
|
||||
new Pure((x) => x?.raw)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -375,32 +368,34 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
|
|||
return json
|
||||
}
|
||||
|
||||
const tagRendering = new TagRenderingConfig(json)
|
||||
try {
|
||||
const tagRendering = new TagRenderingConfig(json)
|
||||
|
||||
const errors = []
|
||||
for (let i = 0; i < tagRendering.mappings.length; i++) {
|
||||
const mapping = tagRendering.mappings[i]
|
||||
if (!mapping.addExtraTags) {
|
||||
continue
|
||||
for (let i = 0; i < tagRendering.mappings.length; i++) {
|
||||
const mapping = tagRendering.mappings[i]
|
||||
if (!mapping.addExtraTags) {
|
||||
continue
|
||||
}
|
||||
const keysInMapping = new Set(mapping.if.usedKeys())
|
||||
|
||||
const keysInAddExtraTags = mapping.addExtraTags.map((t) => t.key)
|
||||
|
||||
const duplicateKeys = keysInAddExtraTags.filter((k) => keysInMapping.has(k))
|
||||
if (duplicateKeys.length > 0) {
|
||||
context
|
||||
.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(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
const keysInMapping = new Set(mapping.if.usedKeys())
|
||||
|
||||
const keysInAddExtraTags = mapping.addExtraTags.map((t) => t.key)
|
||||
|
||||
const duplicateKeys = keysInAddExtraTags.filter((k) => keysInMapping.has(k))
|
||||
if (duplicateKeys.length > 0) {
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
".mappings[" +
|
||||
i +
|
||||
"]: 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(", ")
|
||||
)
|
||||
}
|
||||
return json
|
||||
} catch (e) {
|
||||
context.err(e)
|
||||
return undefined
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -475,8 +470,8 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
|
|||
"some_calculated_tag_value_for_" + calculatedTagName
|
||||
}
|
||||
const parsedConditions = json.mappings.map((m, i) => {
|
||||
const ctx = `${context}.mappings[${i}]`
|
||||
const ifTags = TagUtils.Tag(m.if, ctx)
|
||||
const c = context.enters("mappings", i)
|
||||
const ifTags = TagUtils.Tag(m.if, c.enter("if"))
|
||||
const hideInAnswer = m["hideInAnswer"]
|
||||
if (hideInAnswer !== undefined && hideInAnswer !== false && hideInAnswer !== true) {
|
||||
let conditionTags = TagUtils.Tag(hideInAnswer)
|
||||
|
@ -486,7 +481,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
|
|||
return ifTags
|
||||
})
|
||||
for (let i = 0; i < json.mappings.length; i++) {
|
||||
if (!parsedConditions[i].isUsableAsAnswer()) {
|
||||
if (!parsedConditions[i]?.isUsableAsAnswer()) {
|
||||
// There is no straightforward way to convert this mapping.if into a properties-object, so we simply skip this one
|
||||
// Yes, it might be shadowed, but running this check is to difficult right now
|
||||
continue
|
||||
|
@ -661,12 +656,57 @@ class ValidatePossibleLinks extends DesugaringStep<string | Record<string, strin
|
|||
}
|
||||
}
|
||||
|
||||
class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
||||
private _options: { noQuestionHintCheck: boolean }
|
||||
class CheckTranslation extends DesugaringStep<Translatable> {
|
||||
public static readonly allowUndefined: CheckTranslation = new CheckTranslation(true)
|
||||
public static readonly noUndefined: CheckTranslation = new CheckTranslation()
|
||||
private readonly _allowUndefined: boolean
|
||||
|
||||
constructor(options: { noQuestionHintCheck: boolean }) {
|
||||
constructor(allowUndefined: boolean = false) {
|
||||
super(
|
||||
"Checks that a translation is valid and internally consistent",
|
||||
["*"],
|
||||
"CheckTranslation"
|
||||
)
|
||||
this._allowUndefined = allowUndefined
|
||||
}
|
||||
|
||||
convert(json: Translatable, context: ConversionContext): Translatable {
|
||||
if (json === undefined || json === null) {
|
||||
if (!this._allowUndefined) {
|
||||
context.err("Expected a translation, but got " + json)
|
||||
}
|
||||
return json
|
||||
}
|
||||
if (typeof json === "string") {
|
||||
return json
|
||||
}
|
||||
const keys = Object.keys(json)
|
||||
if (keys.length === 0) {
|
||||
context.err("No actual values are given in this translation, it is completely empty")
|
||||
return json
|
||||
}
|
||||
const en = json["en"]
|
||||
if (!en && json["*"] === undefined) {
|
||||
const msg = "Received a translation without english version"
|
||||
context.warn(msg)
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const lng = json[key]
|
||||
if (lng === "") {
|
||||
context.enter(lng).err("Got an empty string in translation for language " + lng)
|
||||
}
|
||||
|
||||
// TODO validate that all subparts are here
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
||||
constructor() {
|
||||
super("Miscellaneous checks on the tagrendering", ["special"], "MiscTagRenderingChecks")
|
||||
this._options = options
|
||||
}
|
||||
|
||||
convert(
|
||||
|
@ -678,10 +718,42 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
'Detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`'
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
for (const key of ["question", "questionHint", "render"]) {
|
||||
CheckTranslation.allowUndefined.convert(json[key], context.enter(key))
|
||||
}
|
||||
for (let i = 0; i < json.mappings?.length ?? 0; i++) {
|
||||
const mapping = json.mappings[i]
|
||||
CheckTranslation.noUndefined.convert(
|
||||
mapping.then,
|
||||
context.enters("mappings", i, "then")
|
||||
)
|
||||
if (!mapping.if) {
|
||||
context.enters("mappings", i).err("No `if` is defined")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (json["group"]) {
|
||||
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"
|
||||
)
|
||||
}
|
||||
if (json["question"] && !json.freeform && (json.mappings?.length ?? 0) == 1) {
|
||||
context.err("A question is defined, but there is only one option to choose from.")
|
||||
}
|
||||
if (json["questionHint"] && !json["question"]) {
|
||||
context
|
||||
.enter("questionHint")
|
||||
.err(
|
||||
"A questionHint is defined, but no question is given. As such, the questionHint will never be shown"
|
||||
)
|
||||
}
|
||||
|
||||
if (json.freeform) {
|
||||
if (json.render === undefined) {
|
||||
context
|
||||
|
@ -771,16 +843,13 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
|
||||
constructor(
|
||||
layerConfig?: LayerConfigJson,
|
||||
doesImageExist?: DoesImageExist,
|
||||
options?: { noQuestionHintCheck: boolean }
|
||||
) {
|
||||
constructor(layerConfig?: LayerConfigJson, doesImageExist?: DoesImageExist) {
|
||||
super(
|
||||
"Various validation on tagRenderingConfigs",
|
||||
new DetectShadowedMappings(layerConfig),
|
||||
|
@ -790,67 +859,46 @@ 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(options)
|
||||
new MiscTagRenderingChecks()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidateLayer extends Conversion<
|
||||
LayerConfigJson,
|
||||
{ parsed: LayerConfig; raw: LayerConfigJson }
|
||||
> {
|
||||
/**
|
||||
* The paths where this layer is originally saved. Triggers some extra checks
|
||||
* @private
|
||||
*/
|
||||
private readonly _path?: string
|
||||
export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
||||
private readonly _isBuiltin: boolean
|
||||
private readonly _doesImageExist: DoesImageExist
|
||||
/**
|
||||
* The paths where this layer is originally saved. Triggers some extra checks
|
||||
*/
|
||||
private readonly _path: string
|
||||
private readonly _studioValidations: boolean
|
||||
private _skipDefaultLayers: boolean
|
||||
|
||||
constructor(
|
||||
path: string,
|
||||
isBuiltin: boolean,
|
||||
doesImageExist: DoesImageExist,
|
||||
studioValidations: boolean = false,
|
||||
skipDefaultLayers: boolean = false
|
||||
) {
|
||||
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer")
|
||||
constructor(path: string, isBuiltin, doesImageExist, studioValidations) {
|
||||
super("Runs various checks against common mistakes for a layer", [], "PrevalidateLayer")
|
||||
this._path = path
|
||||
this._isBuiltin = isBuiltin
|
||||
this._doesImageExist = doesImageExist
|
||||
this._studioValidations = studioValidations
|
||||
this._skipDefaultLayers = skipDefaultLayers
|
||||
}
|
||||
|
||||
convert(
|
||||
json: LayerConfigJson,
|
||||
context: ConversionContext
|
||||
): { parsed: LayerConfig; raw: LayerConfigJson } {
|
||||
context = context.inOperation(this.name)
|
||||
if (typeof json === "string") {
|
||||
context.err("This layer hasn't been expanded: " + json)
|
||||
return null
|
||||
}
|
||||
|
||||
if (this._skipDefaultLayers && Constants.added_by_default.indexOf(<any>json.id) >= 0) {
|
||||
return { parsed: undefined, raw: json }
|
||||
}
|
||||
|
||||
if (typeof json === "string") {
|
||||
context.err(
|
||||
`Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
|
||||
if (json.id === undefined) {
|
||||
context.enter("id").err(`Not a valid layer: id is undefined`)
|
||||
} else {
|
||||
if (json.id?.toLowerCase() !== json.id) {
|
||||
context.enter("id").err(`The id of a layer should be lowercase: ${json.id}`)
|
||||
}
|
||||
if (json.id?.match(/[a-z0-9-_]/) == null) {
|
||||
context.enter("id").err(`The id of a layer should match [a-z0-9-_]*: ${json.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (json.source === undefined) {
|
||||
context.enter("source").err("No source section is defined")
|
||||
context
|
||||
.enter("source")
|
||||
.err(
|
||||
"No source section is defined; please define one as data is not loaded otherwise"
|
||||
)
|
||||
} else {
|
||||
if (json.source === "special" || json.source === "special:library") {
|
||||
} else if (json.source && json.source["osmTags"] === undefined) {
|
||||
|
@ -884,13 +932,6 @@ export class ValidateLayer extends Conversion<
|
|||
}
|
||||
}
|
||||
|
||||
if (json.id?.toLowerCase() !== json.id) {
|
||||
context.enter("id").err(`The id of a layer should be lowercase: ${json.id}`)
|
||||
}
|
||||
if (json.id?.match(/[a-z0-9-_]/) == null) {
|
||||
context.enter("id").err(`The id of a layer should match [a-z0-9-_]*: ${json.id}`)
|
||||
}
|
||||
|
||||
if (
|
||||
json.syncSelection !== undefined &&
|
||||
LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0
|
||||
|
@ -906,27 +947,6 @@ export class ValidateLayer extends Conversion<
|
|||
)
|
||||
}
|
||||
|
||||
let layerConfig: LayerConfig
|
||||
try {
|
||||
layerConfig = new LayerConfig(json, "validation", true)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
context.err("Could not parse layer due to:" + e)
|
||||
return undefined
|
||||
}
|
||||
for (let i = 0; i < (layerConfig.calculatedTags ?? []).length; i++) {
|
||||
const [_, code, __] = layerConfig.calculatedTags[i]
|
||||
try {
|
||||
new Function("feat", "return " + code + ";")
|
||||
} catch (e) {
|
||||
context
|
||||
.enters("calculatedTags", i)
|
||||
.err(
|
||||
`Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (json.source === "special") {
|
||||
if (!Constants.priviliged_layers.find((x) => x == json.id)) {
|
||||
context.err(
|
||||
|
@ -937,6 +957,10 @@ export class ValidateLayer extends Conversion<
|
|||
}
|
||||
}
|
||||
|
||||
if (context.hasErrors()) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (json.tagRenderings !== undefined && json.tagRenderings.length > 0) {
|
||||
new On("tagRendering", new Each(new ValidateTagRenderings(json)))
|
||||
if (json.title === undefined && json.source !== "special:library") {
|
||||
|
@ -1001,172 +1025,6 @@ export class ValidateLayer extends Conversion<
|
|||
}
|
||||
|
||||
try {
|
||||
if (this._isBuiltin) {
|
||||
// Some checks for legacy elements
|
||||
|
||||
if (json["overpassTags"] !== undefined) {
|
||||
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)'
|
||||
)
|
||||
}
|
||||
const forbiddenTopLevel = [
|
||||
"icon",
|
||||
"wayHandling",
|
||||
"roamingRenderings",
|
||||
"roamingRendering",
|
||||
"label",
|
||||
"width",
|
||||
"color",
|
||||
"colour",
|
||||
"iconOverlays",
|
||||
]
|
||||
for (const forbiddenKey of forbiddenTopLevel) {
|
||||
if (json[forbiddenKey] !== undefined)
|
||||
context.err(
|
||||
"Layer " + json.id + " still has a forbidden key " + forbiddenKey
|
||||
)
|
||||
}
|
||||
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
|
||||
context.err(
|
||||
"Layer " +
|
||||
json.id +
|
||||
" contains an old 'hideUnderlayingFeaturesMinPercentage'"
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
json.isShown !== undefined &&
|
||||
(json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)
|
||||
) {
|
||||
context.warn("Has a tagRendering as `isShown`")
|
||||
}
|
||||
}
|
||||
if (this._isBuiltin) {
|
||||
// Check location of layer file
|
||||
const expected: string = `assets/layers/${json.id}/${json.id}.json`
|
||||
if (this._path != undefined && this._path.indexOf(expected) < 0) {
|
||||
context.err(
|
||||
"Layer is in an incorrect place. The path is " +
|
||||
this._path +
|
||||
", but expected " +
|
||||
expected
|
||||
)
|
||||
}
|
||||
}
|
||||
if (this._isBuiltin) {
|
||||
// Check for correct IDs
|
||||
if (json.tagRenderings?.some((tr) => tr["id"] === "")) {
|
||||
const emptyIndexes: number[] = []
|
||||
for (let i = 0; i < json.tagRenderings.length; i++) {
|
||||
const tagRendering = json.tagRenderings[i]
|
||||
if (tagRendering["id"] === "") {
|
||||
emptyIndexes.push(i)
|
||||
}
|
||||
}
|
||||
context
|
||||
.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")
|
||||
)
|
||||
if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
|
||||
context
|
||||
.enter("tagRenderings")
|
||||
.err(`Some tagRenderings have a duplicate id: ${duplicateIds}`)
|
||||
}
|
||||
|
||||
if (json.description === undefined) {
|
||||
if (typeof json.source === null) {
|
||||
context.err("A priviliged layer must have a description")
|
||||
} else {
|
||||
context.warn("A builtin layer should have a description")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (json.filter) {
|
||||
new On("filter", new Each(new ValidateFilter())).convert(json, context)
|
||||
}
|
||||
|
||||
if (json.tagRenderings !== undefined) {
|
||||
new On(
|
||||
"tagRenderings",
|
||||
new Each(
|
||||
new ValidateTagRenderings(json, this._doesImageExist, {
|
||||
noQuestionHintCheck: json["#"]?.indexOf("no-question-hint-check") >= 0,
|
||||
})
|
||||
)
|
||||
).convert(json, context)
|
||||
}
|
||||
|
||||
if (json.pointRendering !== null && json.pointRendering !== undefined) {
|
||||
if (!Array.isArray(json.pointRendering)) {
|
||||
throw (
|
||||
"pointRendering in " +
|
||||
json.id +
|
||||
" is not iterable, it is: " +
|
||||
typeof json.pointRendering
|
||||
)
|
||||
}
|
||||
for (let i = 0; i < json.pointRendering.length; i++) {
|
||||
const pointRendering = json.pointRendering[i]
|
||||
if (pointRendering.marker === undefined) {
|
||||
continue
|
||||
}
|
||||
for (const icon of pointRendering?.marker) {
|
||||
const indexM = pointRendering?.marker.indexOf(icon)
|
||||
if (!icon.icon) {
|
||||
continue
|
||||
}
|
||||
if (icon.icon["condition"]) {
|
||||
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."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (json.presets !== undefined) {
|
||||
if (typeof json.source === "string") {
|
||||
context.err("A special layer cannot have presets")
|
||||
}
|
||||
// Check that a preset will be picked up by the layer itself
|
||||
const baseTags = TagUtils.Tag(json.source["osmTags"])
|
||||
for (let i = 0; i < json.presets.length; i++) {
|
||||
const preset = json.presets[i]
|
||||
const tags: { k: string; v: string }[] = new And(
|
||||
preset.tags.map((t) => TagUtils.Tag(t))
|
||||
).asChange({ id: "node/-1" })
|
||||
const properties = {}
|
||||
for (const tag of tags) {
|
||||
properties[tag.k] = tag.v
|
||||
}
|
||||
const doMatch = baseTags.matchesProperties(properties)
|
||||
if (!doMatch) {
|
||||
context
|
||||
.enters("presets", i, "tags")
|
||||
.err(
|
||||
"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: " +
|
||||
JSON.stringify(properties) +
|
||||
"\n The required tags are: " +
|
||||
baseTags.asHumanString(false, false, {})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
context.err("Could not validate layer due to: " + e + e.stack)
|
||||
}
|
||||
|
@ -1180,6 +1038,232 @@ export class ValidateLayer extends Conversion<
|
|||
}
|
||||
}
|
||||
|
||||
if (this._isBuiltin) {
|
||||
// Some checks for legacy elements
|
||||
|
||||
if (json["overpassTags"] !== undefined) {
|
||||
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)'
|
||||
)
|
||||
}
|
||||
const forbiddenTopLevel = [
|
||||
"icon",
|
||||
"wayHandling",
|
||||
"roamingRenderings",
|
||||
"roamingRendering",
|
||||
"label",
|
||||
"width",
|
||||
"color",
|
||||
"colour",
|
||||
"iconOverlays",
|
||||
]
|
||||
for (const forbiddenKey of forbiddenTopLevel) {
|
||||
if (json[forbiddenKey] !== undefined)
|
||||
context.err("Layer " + json.id + " still has a forbidden key " + forbiddenKey)
|
||||
}
|
||||
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
|
||||
context.err(
|
||||
"Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'"
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
json.isShown !== undefined &&
|
||||
(json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)
|
||||
) {
|
||||
context.warn("Has a tagRendering as `isShown`")
|
||||
}
|
||||
}
|
||||
if (this._isBuiltin) {
|
||||
// Check location of layer file
|
||||
const expected: string = `assets/layers/${json.id}/${json.id}.json`
|
||||
if (this._path != undefined && this._path.indexOf(expected) < 0) {
|
||||
context.err(
|
||||
"Layer is in an incorrect place. The path is " +
|
||||
this._path +
|
||||
", but expected " +
|
||||
expected
|
||||
)
|
||||
}
|
||||
}
|
||||
if (this._isBuiltin) {
|
||||
// Check for correct IDs
|
||||
if (json.tagRenderings?.some((tr) => tr["id"] === "")) {
|
||||
const emptyIndexes: number[] = []
|
||||
for (let i = 0; i < json.tagRenderings.length; i++) {
|
||||
const tagRendering = json.tagRenderings[i]
|
||||
if (tagRendering["id"] === "") {
|
||||
emptyIndexes.push(i)
|
||||
}
|
||||
}
|
||||
context
|
||||
.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")
|
||||
)
|
||||
if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
|
||||
context
|
||||
.enter("tagRenderings")
|
||||
.err(`Some tagRenderings have a duplicate id: ${duplicateIds}`)
|
||||
}
|
||||
|
||||
if (json.description === undefined) {
|
||||
if (typeof json.source === null) {
|
||||
context.err("A priviliged layer must have a description")
|
||||
} else {
|
||||
context.warn("A builtin layer should have a description")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (json.filter) {
|
||||
new On("filter", new Each(new ValidateFilter())).convert(json, context)
|
||||
}
|
||||
|
||||
if (json.tagRenderings !== undefined) {
|
||||
new On(
|
||||
"tagRenderings",
|
||||
new Each(new ValidateTagRenderings(json, this._doesImageExist))
|
||||
).convert(json, context)
|
||||
}
|
||||
|
||||
if (json.pointRendering !== null && json.pointRendering !== undefined) {
|
||||
if (!Array.isArray(json.pointRendering)) {
|
||||
throw (
|
||||
"pointRendering in " +
|
||||
json.id +
|
||||
" is not iterable, it is: " +
|
||||
typeof json.pointRendering
|
||||
)
|
||||
}
|
||||
for (let i = 0; i < json.pointRendering.length; i++) {
|
||||
const pointRendering = json.pointRendering[i]
|
||||
if (pointRendering.marker === undefined) {
|
||||
continue
|
||||
}
|
||||
for (const icon of pointRendering?.marker) {
|
||||
const indexM = pointRendering?.marker.indexOf(icon)
|
||||
if (!icon.icon) {
|
||||
continue
|
||||
}
|
||||
if (icon.icon["condition"]) {
|
||||
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."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (json.presets !== undefined) {
|
||||
if (typeof json.source === "string") {
|
||||
context.err("A special layer cannot have presets")
|
||||
}
|
||||
// Check that a preset will be picked up by the layer itself
|
||||
const baseTags = TagUtils.Tag(json.source["osmTags"])
|
||||
for (let i = 0; i < json.presets.length; i++) {
|
||||
const preset = json.presets[i]
|
||||
const tags: { k: string; v: string }[] = new And(
|
||||
preset.tags.map((t) => TagUtils.Tag(t))
|
||||
).asChange({ id: "node/-1" })
|
||||
const properties = {}
|
||||
for (const tag of tags) {
|
||||
properties[tag.k] = tag.v
|
||||
}
|
||||
const doMatch = baseTags.matchesProperties(properties)
|
||||
if (!doMatch) {
|
||||
context
|
||||
.enters("presets", i, "tags")
|
||||
.err(
|
||||
"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: " +
|
||||
JSON.stringify(properties) +
|
||||
"\n The required tags are: " +
|
||||
baseTags.asHumanString(false, false, {})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidateLayer extends Conversion<
|
||||
LayerConfigJson,
|
||||
{ parsed: LayerConfig; raw: LayerConfigJson }
|
||||
> {
|
||||
private readonly _skipDefaultLayers: boolean
|
||||
private readonly _prevalidation: PrevalidateLayer
|
||||
|
||||
constructor(
|
||||
path: string,
|
||||
isBuiltin: boolean,
|
||||
doesImageExist: DoesImageExist,
|
||||
studioValidations: boolean = false,
|
||||
skipDefaultLayers: boolean = false
|
||||
) {
|
||||
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer")
|
||||
this._prevalidation = new PrevalidateLayer(
|
||||
path,
|
||||
isBuiltin,
|
||||
doesImageExist,
|
||||
studioValidations
|
||||
)
|
||||
this._skipDefaultLayers = skipDefaultLayers
|
||||
}
|
||||
|
||||
convert(
|
||||
json: LayerConfigJson,
|
||||
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`
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (this._skipDefaultLayers && Constants.added_by_default.indexOf(<any>json.id) >= 0) {
|
||||
return { parsed: undefined, raw: json }
|
||||
}
|
||||
|
||||
this._prevalidation.convert(json, context.inOperation(this._prevalidation.name))
|
||||
|
||||
if (context.hasErrors()) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let layerConfig: LayerConfig
|
||||
try {
|
||||
layerConfig = new LayerConfig(json, "validation", true)
|
||||
} catch (e) {
|
||||
context.err("Could not parse layer due to:" + e)
|
||||
return undefined
|
||||
}
|
||||
for (let i = 0; i < (layerConfig.calculatedTags ?? []).length; i++) {
|
||||
const [_, code, __] = layerConfig.calculatedTags[i]
|
||||
try {
|
||||
new Function("feat", "return " + code + ";")
|
||||
} catch (e) {
|
||||
context
|
||||
.enters("calculatedTags", i)
|
||||
.err(
|
||||
`Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return { raw: json, parsed: layerConfig }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -198,6 +198,8 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
freeform?: {
|
||||
/**
|
||||
* question: What is the name of the attribute that should be written to?
|
||||
* This is the OpenStreetMap-key that that value will be written to
|
||||
*
|
||||
* ifunset: do not offer a freeform textfield as answer option
|
||||
*/
|
||||
key: string
|
||||
|
@ -215,7 +217,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
* A (translated) text that is shown (as gray text) within the textfield
|
||||
* type: translation
|
||||
*/
|
||||
placeholder?: string | any
|
||||
placeholder?: Translatable
|
||||
|
||||
/**
|
||||
* Extra parameters to initialize the input helper arguments.
|
||||
|
@ -259,7 +261,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
*
|
||||
* ifunset: This tagrendering will be shown if it is known, but cannot be edited by the contributor, effectively resutling in a read-only rendering
|
||||
*/
|
||||
question?: string | Translatable
|
||||
question?: Translatable
|
||||
|
||||
/**
|
||||
* question: Should some extra information be shown to the contributor, alongside the question?
|
||||
|
@ -267,7 +269,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
* This can give some extra information on what the answer should ook like
|
||||
* ifunset: No extra hint is given
|
||||
*/
|
||||
questionHint?: string | Translatable
|
||||
questionHint?: Translatable
|
||||
|
||||
/**
|
||||
* A list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer
|
||||
|
|
|
@ -9,7 +9,8 @@ import { Utils } from "../../Utils"
|
|||
import LanguageUtils from "../../Utils/LanguageUtils"
|
||||
|
||||
import { RasterLayerProperties } from "../RasterLayerProperties"
|
||||
import { ConversionContext } from "./Conversion/Conversion"
|
||||
|
||||
import { ConversionContext } from "./Conversion/ConversionContext"
|
||||
|
||||
/**
|
||||
* Minimal information about a theme
|
||||
|
|
|
@ -240,10 +240,6 @@ export default class TagRenderingConfig {
|
|||
)
|
||||
}
|
||||
|
||||
if (this.question && this.freeform?.key === undefined && this.mappings === undefined) {
|
||||
throw `${context}: A question is defined, but no mappings nor freeform (key) are. The question is ${this.question.txt} at ${context}`
|
||||
}
|
||||
|
||||
if (!json.multiAnswer && this.mappings !== undefined && this.question !== undefined) {
|
||||
let keys = []
|
||||
for (let i = 0; i < this.mappings.length; i++) {
|
||||
|
@ -315,7 +311,7 @@ export default class TagRenderingConfig {
|
|||
) {
|
||||
const ctx = `${translationKey}.mappings.${i}`
|
||||
if (mapping.if === undefined) {
|
||||
throw `${ctx}: Invalid mapping: "if" is not defined in ${JSON.stringify(mapping)}`
|
||||
throw `Invalid mapping: "if" is not defined`
|
||||
}
|
||||
if (mapping.then === undefined) {
|
||||
if (mapping["render"] !== undefined) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue