From 8b0ba633f68dacf9bd081853e75f61d031db970e Mon Sep 17 00:00:00 2001 From: Arkerthan <arkerthan@mailbox.org> Date: Wed, 22 Jun 2022 17:48:25 +0200 Subject: [PATCH] Add a simple mode to the RA condition editor --- devTools/types/FC/RA.d.ts | 6 +- js/rulesAssistant/conditionEditorSimple.js | 335 ++++++++++++++++++ js/rulesAssistant/conditionEditorTree.js | 11 +- .../z1-conditionEditorController.js | 78 +++- .../backwardsCompatibility/datatypeCleanup.js | 4 + src/js/rulesAssistant.js | 1 + src/js/rulesAssistantOptions.js | 4 +- 7 files changed, 417 insertions(+), 22 deletions(-) create mode 100644 js/rulesAssistant/conditionEditorSimple.js diff --git a/devTools/types/FC/RA.d.ts b/devTools/types/FC/RA.d.ts index d9c75fff29f..4552816cf06 100644 --- a/devTools/types/FC/RA.d.ts +++ b/devTools/types/FC/RA.d.ts @@ -8,8 +8,12 @@ declare namespace FC { type NumericTarget = GenericNumericTarget<number>; type ExpressiveNumericTarget = GenericNumericTarget<number | string>; - interface RuleConditions { + interface RuleConditionEditorArguments { activation: PostFixRule; + advancedMode: boolean; + } + + interface RuleConditions extends RuleConditionEditorArguments{ selectedSlaves: number[]; excludedSlaves: number[]; applyRuleOnce: boolean; diff --git a/js/rulesAssistant/conditionEditorSimple.js b/js/rulesAssistant/conditionEditorSimple.js new file mode 100644 index 00000000000..0dd064745d8 --- /dev/null +++ b/js/rulesAssistant/conditionEditorSimple.js @@ -0,0 +1,335 @@ +/** + * All functions should only be called from z1-conditionEditorController.js + */ +App.RA.Activation.SimpleEditor = (function() { + /** + * @typedef {object} RuleState + * @property {"always"|"never"|"boolean"|"number"|"string"} activeRuleType + * @property {string} boolGetter + * @property {boolean} negateBool + * @property {string} stringGetter + * @property {string} stringComparator + * @property {string} stringValue + * @property {string} numberGetter + * @property {number} numberUpperValue + * @property {"lt"|"lte"|""} numberUpperComparator + * @property {number} numberLowerValue + * @property {"gt"|"gte"|""} numberLowerComparator + */ + + /** + * @type {HTMLDivElement} + */ + let editorNode = null; + /** + * @type {RuleState} + */ + let currentRule = null; + + /** + * @param {FC.RA.PostFixRule} rule + * @param {HTMLDivElement}parent + */ + function editor(rule, parent) { + currentRule = deserializeRule(rule); + editorNode = parent; + editorNode.append(buildEditor()); + } + + function refreshEditor() { + if (editorNode !== null) { + $(editorNode).empty().append(buildEditor()); + } + } + + /** + * If the rule is valid, returns the serialized rule, otherwise null. + * + * @returns {FC.RA.PostFixRule} + */ + function saveEditor() { + if (currentRule == null) { + return null; + } + return serializeRule(currentRule); + } + + function resetEditor() { + currentRule = null; + editorNode = null; + } + + /** + * @returns {HTMLElement} + */ + function buildEditor() { + const outerDiv = document.createElement("div"); + // selector + const selectorDiv = document.createElement("div"); + selectorDiv.classList.add("button-group"); + outerDiv.append(selectorDiv); + App.UI.DOM.appendNewElement("button", selectorDiv, "Always", currentRule.activeRuleType === "always" ? ["selected", "disabled"] : []).onclick = () => { + currentRule.activeRuleType = "always"; + refreshEditor(); + }; + App.UI.DOM.appendNewElement("button", selectorDiv, "Never", currentRule.activeRuleType === "never" ? ["selected", "disabled"] : []).onclick = () => { + currentRule.activeRuleType = "never"; + refreshEditor(); + }; + App.UI.DOM.appendNewElement("button", selectorDiv, "Boolean", currentRule.activeRuleType === "boolean" ? ["selected", "disabled"] : []).onclick = () => { + currentRule.activeRuleType = "boolean"; + refreshEditor(); + }; + App.UI.DOM.appendNewElement("button", selectorDiv, "Number", currentRule.activeRuleType === "number" ? ["selected", "disabled"] : []).onclick = () => { + currentRule.activeRuleType = "number"; + refreshEditor(); + }; + App.UI.DOM.appendNewElement("button", selectorDiv, "String", currentRule.activeRuleType === "string" ? ["selected", "disabled"] : []).onclick = () => { + currentRule.activeRuleType = "string"; + refreshEditor(); + }; + + // add bool + if (currentRule.activeRuleType === "boolean") { + const boolDiv = document.createElement("div"); + boolDiv.classList.add("button-group"); + outerDiv.append(boolDiv); + /** + * @type {selectOption[]} + */ + const options = []; + App.RA.Activation.getterManager.booleanGetters.forEach((getter, key) => { + if (!getter.visible || getter.visible()) { + options.push({ + key: key, name: getter.name, enabled: !getter.enabled || getter.enabled() + }); + } + }); + boolDiv.append(App.UI.DOM.makeSelect(options, currentRule.boolGetter, key => { + currentRule.boolGetter = key; + refreshEditor(); + })); + boolDiv.append(" should be "); + App.UI.DOM.appendNewElement("button", boolDiv, "True", currentRule.negateBool ? [] : ["selected", "disabled"]).onclick = () => { + currentRule.negateBool = false; + refreshEditor(); + }; + App.UI.DOM.appendNewElement("button", boolDiv, "False", currentRule.negateBool ? ["selected", "disabled"] : []).onclick = () => { + currentRule.negateBool = true; + refreshEditor(); + }; + } else if (currentRule.activeRuleType === "number") { + const numberDiv = document.createElement("div"); + outerDiv.append(numberDiv); + /** + * @type {selectOption[]} + */ + const options = []; + App.RA.Activation.getterManager.numberGetters.forEach((getter, key) => { + if (!getter.visible || getter.visible()) { + options.push({ + key: key, name: getter.name, enabled: !getter.enabled || getter.enabled() + }); + } + }); + numberDiv.append(App.UI.DOM.makeSelect(options, currentRule.numberGetter, key => { + currentRule.numberGetter = key; + refreshEditor(); + })); + numberDiv.append(" should be "); + numberDiv.append(App.UI.DOM.makeSelect( + [{key: "gt", name: "greater than"}, {key: "gte", name: "greater than or equal to"}, + {key: "", name: "ignored"}], + currentRule.numberLowerComparator, key => { + currentRule.numberLowerComparator = key; + refreshEditor(); + })); + if (currentRule.numberLowerComparator !== "") { + numberDiv.append(" ", App.UI.DOM.makeTextBox(currentRule.numberLowerValue, val => { + currentRule.numberLowerValue = val; + }, true)); + } + numberDiv.append(" and "); + numberDiv.append(App.UI.DOM.makeSelect( + [{key: "lt", name: "less than"}, {key: "lte", name: "less than or equal to"}, + {key: "", name: "ignored"}], + currentRule.numberUpperComparator, key => { + currentRule.numberUpperComparator = key; + refreshEditor(); + })); + if (currentRule.numberUpperComparator !== "") { + numberDiv.append(" ", App.UI.DOM.makeTextBox(currentRule.numberUpperValue, val => { + currentRule.numberUpperValue = val; + }, true)); + } + } else if (currentRule.activeRuleType === "string") { + const stringDiv = document.createElement("div"); + outerDiv.append(stringDiv); + /** + * @type {selectOption[]} + */ + const options = []; + App.RA.Activation.getterManager.stringGetters.forEach((getter, key) => { + if (!getter.visible || getter.visible()) { + options.push({ + key: key, name: getter.name, enabled: !getter.enabled || getter.enabled() + }); + } + }); + stringDiv.append(App.UI.DOM.makeSelect(options, currentRule.stringGetter, key => { + currentRule.stringGetter = key; + refreshEditor(); + })); + stringDiv.append(" should "); + stringDiv.append(App.UI.DOM.makeSelect( + [{key: "eqstr", name: "equal"}, {key: "substr", name: "contain"}, {key: "match", name: "match"}], + currentRule.stringComparator, key => { + currentRule.stringComparator = key; + refreshEditor(); + })); + stringDiv.append(" ", App.UI.DOM.makeTextBox(currentRule.stringValue, val => { + currentRule.stringValue = val; + })); + } + + return outerDiv; + } + + /** + * @param {FC.RA.PostFixRule} rule + * @returns {RuleState} + */ + function deserializeRule(rule) { + // About the TS errors in this function: we can assume a lot about the rule composition because we know it's in + // the simple format. The rule itself is still a normal FC.RA.PostFixRule which would allow a lot more. + // Therefore, TS is not happy even though we now everything's fine. + /** + * @type {RuleState} + */ + const ruleState = { + activeRuleType: "always", + boolGetter: "isfertile", + negateBool: false, + stringGetter: "label", + stringValue: "", + stringComparator: "eqstr", + numberGetter: "devotion", + numberUpperValue: 100, + numberUpperComparator: "", + numberLowerValue: -100, + numberLowerComparator: "" + }; + // we know there is only one rule. + let i = 0; + const rulePart = rule[i]; + if (rulePart === true) { + ruleState.activeRuleType = "always"; + } else if (rulePart === false) { + ruleState.activeRuleType = "never"; + } else if (App.RA.Activation.getterManager.isBoolean(rulePart)) { + ruleState.activeRuleType = "boolean"; + ruleState.boolGetter = rulePart; + if (rule[i + 1] === "not") { + ruleState.negateBool = true; + i++; + } + } else if (App.RA.Activation.getterManager.isNumber(rulePart)) { + ruleState.activeRuleType = "number"; + ruleState.numberGetter = rulePart; + // check if there is a lower rule: + if (rule[i + 2].startsWith("g")) { + ruleState.numberLowerValue = rule[i + 1]; + ruleState.numberLowerComparator = rule[i + 2]; + // check if there is also an upper value: + if (rule[i + 3] === ruleState.numberGetter) { + ruleState.numberUpperValue = rule[i + 4]; + ruleState.numberUpperComparator = rule[i + 5]; + i += 3; + } + } else { + ruleState.numberUpperValue = rule[i + 1]; + ruleState.numberUpperComparator = rule[i + 2]; + } + i += 2; + } else if (App.RA.Activation.getterManager.isString(rulePart)) { + ruleState.activeRuleType = "string"; + ruleState.stringGetter = rulePart; + ruleState.stringValue = rule[i + 1].slice(1); + ruleState.stringComparator = rule[i + 2]; + i += 2; + } else { + throw new Error("Rule is not in simple mode format!"); + } + i++; + + return ruleState; + } + + /** + * Expects a valid RulePart structure + * + * @param {RuleState} ruleState + * @returns {FC.RA.PostFixRule} + */ + function serializeRule(ruleState) { + /** + * @type {FC.RA.PostFixRule} + */ + const rule = []; + let counter = 0; + + switch (ruleState.activeRuleType) { + case "always": + rule.push(true); + counter++; + break; + case "never": + rule.push(false); + counter++; + break; + case "boolean": + rule.push(ruleState.boolGetter); + if (ruleState.negateBool) { + rule.push("not"); + } + counter++; + break; + case "number": + // eslint-disable-next-line no-case-declarations + let any = false; + if (ruleState.numberLowerComparator !== "") { + any = true; + rule.push(ruleState.numberGetter); + rule.push(ruleState.numberLowerValue); + rule.push(ruleState.numberLowerComparator); + counter++; + } + if (ruleState.numberUpperComparator !== "") { + any = true; + rule.push(ruleState.numberGetter); + rule.push(ruleState.numberUpperValue); + rule.push(ruleState.numberUpperComparator); + counter++; + } + if (!any) { + rule.push(true); + counter++; + } + break; + case "string": + rule.push(ruleState.stringGetter); + rule.push("!"+ruleState.stringValue); + rule.push(ruleState.stringComparator); + counter++; + break; + } + rule.push(counter, "and"); + return rule; + } + + return { + build: editor, + save: saveEditor, + reset: resetEditor, + }; +})(); diff --git a/js/rulesAssistant/conditionEditorTree.js b/js/rulesAssistant/conditionEditorTree.js index 49cbb8e46fa..87b8831b4c8 100644 --- a/js/rulesAssistant/conditionEditorTree.js +++ b/js/rulesAssistant/conditionEditorTree.js @@ -33,18 +33,19 @@ App.RA.Activation.TreeEditor = (function() { } /** - * Save the rule, if it is valid. + * If the rule is valid, returns the serialized rule, otherwise null. * - * @param {(rule:FC.RA.PostFixRule)=>void} callback + * @returns {FC.RA.PostFixRule} */ - function saveEditor(callback) { + function saveEditor() { if (currentRule == null) { - return; + return null; } const error = currentRule.validate([]) === "error"; if (!error) { - callback(serializeRule(currentRule)); + return serializeRule(currentRule); } + return null; } function resetEditor() { diff --git a/js/rulesAssistant/z1-conditionEditorController.js b/js/rulesAssistant/z1-conditionEditorController.js index abf3b05fd9d..a45aa13f462 100644 --- a/js/rulesAssistant/z1-conditionEditorController.js +++ b/js/rulesAssistant/z1-conditionEditorController.js @@ -1,41 +1,91 @@ App.RA.Activation.Editor = (function() { - let advanced = true; /** + * Should the advanced mode (tree editor) be used? + * @type {boolean} + */ + let advanced = false; + /** + * Keep a reference to the outermost node, so we can refresh it when needed. * @type {HTMLDivElement} */ - let editorNode = null; + let outerNode = null; /** - * @param {FC.RA.PostFixRule} rule + * @param {FC.RA.RuleConditionEditorArguments} args * @returns {HTMLDivElement} */ - function editor(rule) { - editorNode = document.createElement("div"); + function editor(args) { + outerNode = document.createElement("div"); + fillOuterNode(args); + return outerNode; + } + + /** + * @param {FC.RA.RuleConditionEditorArguments} args + */ + function fillOuterNode(args) { + advanced = args.advancedMode; + let editorNode = document.createElement("div"); if (advanced) { - App.RA.Activation.TreeEditor.build(rule, editorNode); + outerNode.append(App.UI.DOM.link("Reset to simple mode", () => { + if (SugarCube.Dialog.isOpen()) { + SugarCube.Dialog.close(); + } + SugarCube.Dialog.setup("Reset RA to simple mode"); + $(SugarCube.Dialog.body()).empty().append( + "<p>Resetting will delete your current conditions. Do you want to continue?</p>", + App.UI.DOM.link("Yes, delete conditions.", () => { + args.advancedMode = false; + args.activation = App.RA.newRule.conditions().activation; + SugarCube.Dialog.close(); + $(outerNode).empty(); + fillOuterNode(args); + }), " ", + App.UI.DOM.makeElement("p", App.UI.DOM.link("Abort.", () => { + SugarCube.Dialog.close(); + }))); + SugarCube.Dialog.open(); + })); + App.RA.Activation.TreeEditor.build(args.activation, editorNode); } else { - // TODO basic + outerNode.append(App.UI.DOM.link("Switch to advanced mode", () => { + args.advancedMode = true; + $(outerNode).empty(); + fillOuterNode(args); + })); + App.RA.Activation.SimpleEditor.build(args.activation, editorNode); } - return editorNode; + outerNode.append(editorNode); } /** * Save the rule, if it is valid. * - * @param {(rule:FC.RA.PostFixRule)=>void} callback + * @param {FC.RA.RuleConditionEditorArguments} args */ - function saveEditor(callback) { + function saveEditor(args) { if (advanced) { - App.RA.Activation.TreeEditor.save(callback); + let rule = App.RA.Activation.TreeEditor.save(); + if (rule == null) { + return; + } + args.advancedMode = advanced; + args.activation = rule; } else { - // TODO basic + let rule = App.RA.Activation.SimpleEditor.save(); + if (rule == null) { + return; + } + args.advancedMode = advanced; + args.activation = rule; } } function resetEditor() { App.RA.Activation.TreeEditor.reset(); - // TODO basic - editorNode = null; + App.RA.Activation.SimpleEditor.reset(); + outerNode = null; + advanced = false; } return { diff --git a/src/data/backwardsCompatibility/datatypeCleanup.js b/src/data/backwardsCompatibility/datatypeCleanup.js index af14361979d..6c747999a1e 100644 --- a/src/data/backwardsCompatibility/datatypeCleanup.js +++ b/src/data/backwardsCompatibility/datatypeCleanup.js @@ -2305,6 +2305,10 @@ App.Entity.Utils.RARuleDatatypeCleanup = function() { }; return jobs[assignment]; } + + if (!cond.hasOwnProperty("advancedMode")) { + cond.advancedMode = true; + } } /** @param {object} o */ diff --git a/src/js/rulesAssistant.js b/src/js/rulesAssistant.js index 66be45512f3..c247619be5f 100644 --- a/src/js/rulesAssistant.js +++ b/src/js/rulesAssistant.js @@ -178,6 +178,7 @@ App.RA.newRule = function() { function emptyConditions() { return { activation: ["devotion", 20, "gt", 1, "and"], + advancedMode: false, selectedSlaves: [], excludedSlaves: [], applyRuleOnce: false, diff --git a/src/js/rulesAssistantOptions.js b/src/js/rulesAssistantOptions.js index d47e17530bd..ebc47642f37 100644 --- a/src/js/rulesAssistantOptions.js +++ b/src/js/rulesAssistantOptions.js @@ -105,7 +105,7 @@ App.RA.options = (function() { * Save the settings for this rule. */ function saveSettings() { - App.RA.Activation.Editor.save(cond => current_rule.condition.activation = cond); + App.RA.Activation.Editor.save(current_rule.condition); } const parse = { @@ -1294,7 +1294,7 @@ App.RA.options = (function() { class ConditionBuilder extends Element { render() { - return App.RA.Activation.Editor.build(current_rule.condition.activation); + return App.RA.Activation.Editor.build(current_rule.condition); } } -- GitLab