Add question box as special rendering

This commit is contained in:
Pieter Vander Vennet 2023-03-31 03:28:11 +02:00
parent 15664df63f
commit d47fd7e746
42 changed files with 956 additions and 311 deletions

View file

@ -23,6 +23,8 @@ import predifined_filters from "../../../assets/layers/filters/filters.json"
import { TagConfigJson } from "../Json/TagConfigJson"
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
import ValidationUtils from "./ValidationUtils"
import { RenderingSpecification } from "../../../UI/SpecialVisualization"
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
private static readonly predefinedFilters = ExpandFilter.load_filters()
@ -218,7 +220,7 @@ class ExpandTagRendering extends Conversion<
matchingTrs = layerTrs
} else if (id.startsWith("*")) {
const id_ = id.substring(1)
matchingTrs = layerTrs.filter((tr) => tr.group === id_ || tr.labels?.indexOf(id_) >= 0)
matchingTrs = layerTrs.filter((tr) => tr.labels?.indexOf(id_) >= 0)
} else {
matchingTrs = layerTrs.filter((tr) => tr.id === id || tr.labels?.indexOf(id) >= 0)
}
@ -255,13 +257,6 @@ class ExpandTagRendering extends Conversion<
ctx: string
): TagRenderingConfigJson[] {
const state = this._state
if (tr === "questions") {
return [
{
id: "questions",
},
]
}
if (typeof tr === "string") {
const lookup = this.lookup(tr)
@ -415,6 +410,111 @@ class ExpandTagRendering extends Conversion<
}
}
export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
constructor() {
super(
"Adds a 'questions'-object if no question element is added yet",
["tagRenderings"],
"AddQuestionBox"
)
}
convert(
json: LayerConfigJson,
context: string
): { result: LayerConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
if (json.tagRenderings === undefined) {
return { result: json }
}
json = JSON.parse(JSON.stringify(json))
const allSpecials: Exclude<RenderingSpecification, string>[] = []
.concat(
...json.tagRenderings.map((tr) =>
ValidationUtils.getSpecialVisualsationsWithArgs(<TagRenderingConfigJson>tr)
)
)
.filter((spec) => typeof spec !== "string")
const questionSpecials = allSpecials.filter((sp) => sp.func.funcName === "questions")
const noLabels = questionSpecials.filter(
(sp) => sp.args.length === 0 || sp.args[0].trim() === ""
)
const errors: string[] = []
const warnings: string[] = []
if (noLabels.length > 1) {
console.log(json.tagRenderings)
errors.push(
"At " +
context +
": multiple 'questions'-visualisations found which would show _all_ questions. Don't do this"
)
}
// ALl labels that are used in this layer
const allLabels = new Set(
[].concat(...json.tagRenderings.map((tr) => (<TagRenderingConfigJson>tr).labels ?? []))
)
const seen = new Set()
for (const questionSpecial of questionSpecials) {
if (typeof questionSpecial === "string") {
continue
}
const used = questionSpecial.args[0]
?.split(";")
?.map((a) => a.trim())
?.filter((s) => s != "")
const blacklisted = questionSpecial.args[1]
?.split(";")
?.map((a) => a.trim())
?.filter((s) => s != "")
if (blacklisted?.length > 0 && used?.length > 0) {
errors.push(
"At " +
context +
": the {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." +
"\n Whitelisted: " +
used.join(", ") +
"\n Blacklisted: " +
blacklisted.join(", ")
)
}
for (const usedLabel of used) {
if (!allLabels.has(usedLabel)) {
errors.push(
"At " +
context +
": this layers specifies a special question element for label `" +
usedLabel +
"`, but this label doesn't exist.\n" +
" Available labels are " +
Array.from(allLabels).join(", ")
)
}
seen.add(usedLabel)
}
}
if (noLabels.length == 0) {
/* At this point, we know which question labels are not yet handled and which already are handled, and we
* know there is no previous catch-all questions
*/
const question: TagRenderingConfigJson = {
id: "leftover-questions",
render: {
"*": `{questions( ,${Array.from(seen).join(";")})}`,
},
}
json.tagRenderings.push(question)
}
return {
result: json,
errors,
warnings,
}
}
}
export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[]> {
constructor() {
super("Applies a rewrite", [], "ExpandRewrite")

View file

@ -10,7 +10,7 @@ import {
SetDefault,
} from "./Conversion"
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
import { PrepareLayer } from "./PrepareLayer"
import { AddQuestionBox, PrepareLayer } from "./PrepareLayer"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { Utils } from "../../../Utils"
import Constants from "../../Constants"
@ -336,7 +336,6 @@ export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
if (!hasMinimap) {
layerConfig = { ...layerConfig }
layerConfig.tagRenderings = [...layerConfig.tagRenderings]
layerConfig.tagRenderings.push(state.tagRenderings.get("questions"))
layerConfig.tagRenderings.push(state.tagRenderings.get("minimap"))
}
@ -662,6 +661,7 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
: new AddDefaultLayers(state),
new AddDependencyLayersToTheme(state),
new AddImportLayers(),
new On("layers", new Each(new AddQuestionBox())),
new On("layers", new Each(new AddMiniMap(state)))
)
}

View file

@ -619,12 +619,12 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
': detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`'
)
}
if (json.group) {
if (json["group"]) {
errors.push(
"At " +
context +
': groups are deprecated, use `"label": ["' +
json.group +
json["group"] +
'"]` instead'
)
}

View file

@ -1,7 +1,7 @@
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
import { SpecialVisualization } from "../../../UI/SpecialVisualization"
import { RenderingSpecification, SpecialVisualization } from "../../../UI/SpecialVisualization"
export default class ValidationUtils {
/**
@ -11,11 +11,18 @@ export default class ValidationUtils {
public static getSpecialVisualisations(
renderingConfig: TagRenderingConfigJson
): SpecialVisualization[] {
return ValidationUtils.getSpecialVisualsationsWithArgs(renderingConfig).map(
(spec) => spec["func"]
)
}
public static getSpecialVisualsationsWithArgs(
renderingConfig: TagRenderingConfigJson
): RenderingSpecification[] {
const translations: any[] = Utils.NoNull([
renderingConfig.render,
...(renderingConfig.mappings ?? []).map((m) => m.then),
])
const all: SpecialVisualization[] = []
const all: RenderingSpecification[] = []
for (let translation of translations) {
if (typeof translation == "string") {
translation = { "*": translation }
@ -28,9 +35,7 @@ export default class ValidationUtils {
const template = translation[key]
const parts = SpecialVisualizations.constructSpecification(template)
const specials = parts
.filter((p) => typeof p !== "string")
.map((special) => special["func"])
const specials = parts.filter((p) => typeof p !== "string")
all.push(...specials)
}
}

View file

@ -244,8 +244,8 @@ export default class PointRenderingConfig extends WithContextLoader {
iconAndBadges.SetClass("w-full h-full")
}
const css = this.cssDef?.GetRenderValue(tags, undefined)?.txt
const cssClasses = this.cssClasses?.GetRenderValue(tags, undefined)?.txt
const css = this.cssDef?.GetRenderValue(tags)?.txt
const cssClasses = this.cssClasses?.GetRenderValue(tags)?.txt
let label = this.GetLabel(tags)
let htmlEl: BaseUIElement

View file

@ -203,19 +203,6 @@ export default class TagRenderingConfig {
throw `${context}: A question is defined, but no mappings nor freeform (key) are. The question is ${this.question.txt} at ${context}`
}
if (this.id === "questions" && this.render !== undefined) {
for (const ln in this.render.translations) {
const txt: string = this.render.translations[ln]
if (txt.indexOf("{questions}") >= 0) {
continue
}
throw `${context}: The rendering for language ${ln} does not contain {questions}. This is a bug, as this rendering should include exactly this to trigger those questions to be shown!`
}
if (this.freeform?.key !== undefined && this.freeform?.key !== "questions") {
throw `${context}: If the ID is questions to trigger a question box, the only valid freeform value is 'questions' as well. Set freeform to questions or remove the freeform all together`
}
}
if (this.freeform) {
if (this.render === undefined) {
throw `${context}: Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}`
@ -509,15 +496,11 @@ export default class TagRenderingConfig {
})
}
}
return applicableMappings
}
public GetRenderValue(
tags: any,
defltValue: any = undefined
): TypedTranslation<any> | undefined {
return this.GetRenderValueWithImage(tags, defltValue)?.then
public GetRenderValue(tags: Record<string, string>): TypedTranslation<any> | undefined {
return this.GetRenderValueWithImage(tags)?.then
}
/**
@ -526,8 +509,7 @@ export default class TagRenderingConfig {
* @constructor
*/
public GetRenderValueWithImage(
tags: any,
defltValue: any = undefined
tags: Record<string, string>
): { then: TypedTranslation<any>; icon?: string } | undefined {
if (this.condition !== undefined) {
if (!this.condition.matchesProperties(tags)) {
@ -554,7 +536,7 @@ export default class TagRenderingConfig {
return { then: this.render }
}
return { then: defltValue }
return undefined
}
/**
@ -625,6 +607,76 @@ export default class TagRenderingConfig {
}
}
/**
* Given a value for the freeform key and an overview of the selected mappings, construct the correct tagsFilter to apply
*
* @param freeformValue The freeform value which will be applied as 'freeform.key'. Ignored if 'freeform.key' is not set
*
* @param singleSelectedMapping (Only used if multiAnswer == false): the single mapping to apply. Use (mappings.length) for the freeform
* @param multiSelectedMapping (Only used if multiAnswer == true): all the mappings that must be applied. Set multiSelectedMapping[mappings.length] to use the freeform as well
*/
public constructChangeSpecification(
freeformValue: string | undefined,
singleSelectedMapping: number,
multiSelectedMapping: boolean[] | undefined
): UploadableTag {
if (
freeformValue === undefined &&
singleSelectedMapping === undefined &&
multiSelectedMapping === undefined
) {
return undefined
}
if (this.mappings === undefined && freeformValue === undefined) {
return undefined
}
if (
this.freeform !== undefined &&
(this.mappings === undefined ||
this.mappings.length == 0 ||
(singleSelectedMapping === this.mappings.length && !this.multiAnswer))
) {
// Either no mappings, or this is a radio-button selected freeform value
return new And([
new Tag(this.freeform.key, freeformValue),
...(this.freeform.addExtraTags ?? []),
])
}
if (this.multiAnswer) {
let selectedMappings: UploadableTag[] = this.mappings
.filter((_, i) => multiSelectedMapping[i])
.map((m) => new And([m.if, ...(m.addExtraTags ?? [])]))
let unselectedMappings: UploadableTag[] = this.mappings
.filter((_, i) => !multiSelectedMapping[i])
.map((m) => m.ifnot)
if (multiSelectedMapping.at(-1)) {
// The freeform value was selected as well
selectedMappings.push(
new And([
new Tag(this.freeform.key, freeformValue),
...(this.freeform.addExtraTags ?? []),
])
)
}
return TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings])
} else {
if (singleSelectedMapping === this.mappings.length) {
return new And([
new Tag(this.freeform.key, freeformValue),
...(this.freeform.addExtraTags ?? []),
])
} else {
return new And([
this.mappings[singleSelectedMapping].if,
...(this.mappings[singleSelectedMapping].addExtraTags ?? []),
])
}
}
}
GenerateDocumentation(): BaseUIElement {
let withRender: (BaseUIElement | string)[] = []
if (this.freeform?.key !== undefined) {