migrate checkin rules editor to vue3

- move constants to a module
- move reading from and writing to non-vue html to django interop module
- switch to composition api and script setup sfc with pug
- use optional chaining operators a lot to simplify code
This commit is contained in:
rash
2026-02-03 15:02:32 +01:00
parent 3d37f62c51
commit ee44d4c968
18 changed files with 1584 additions and 1538 deletions

5
.editorconfig Normal file
View File

@@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = tab
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

26
package-lock.json generated
View File

@@ -14,6 +14,8 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@stylistic/eslint-plugin": "^5.7.1", "@stylistic/eslint-plugin": "^5.7.1",
"@types/jquery": "^3.5.33",
"@types/moment": "^2.11.29",
"@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-typescript": "^14.6.0", "@vue/eslint-config-typescript": "^14.6.0",
"eslint": "^9.39.2", "eslint": "^9.39.2",
@@ -1223,6 +1225,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/jquery": {
"version": "3.5.33",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.33.tgz",
"integrity": "sha512-SeyVJXlCZpEki5F0ghuYe+L+PprQta6nRZqhONt9F13dWBtR/ftoaIbdRQ7cis7womE+X2LKhsDdDtkkDhJS6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/sizzle": "*"
}
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1230,6 +1242,20 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/moment": {
"version": "2.11.29",
"resolved": "https://registry.npmjs.org/@types/moment/-/moment-2.11.29.tgz",
"integrity": "sha512-D5WIgbLYQzvgfsDnBhZFSTnt/BjGPOE+Jsh3k1BYYijJAkrn7ceeLvU4jtjKKXXuXN42O3ARlU4D/P9ezbQYFA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/sizzle": {
"version": "2.3.10",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz",
"integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.54.0", "version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz",

View File

@@ -29,6 +29,8 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@stylistic/eslint-plugin": "^5.7.1", "@stylistic/eslint-plugin": "^5.7.1",
"@types/jquery": "^3.5.33",
"@types/moment": "^2.11.29",
"@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-typescript": "^14.6.0", "@vue/eslint-config-typescript": "^14.6.0",
"eslint": "^9.39.2", "eslint": "^9.39.2",

View File

@@ -3,6 +3,7 @@
{% load bootstrap3 %} {% load bootstrap3 %}
{% load static %} {% load static %}
{% load compress %} {% load compress %}
{% load vite %}
{% block title %} {% block title %}
{% if checkinlist %} {% if checkinlist %}
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %} {% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
@@ -74,45 +75,8 @@
{% bootstrap_field form.ignore_in_statistics layout="control" %} {% bootstrap_field form.ignore_in_statistics layout="control" %}
<h3>{% trans "Custom check-in rule" %}</h3> <h3>{% trans "Custom check-in rule" %}</h3>
<div id="rules-editor" class="form-inline"> <div id="rules-editor">
<div> <!-- Vue app mount point -->
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
<a href="#rules-edit" role="tab" data-toggle="tab">
<span class="fa fa-edit"></span>
{% trans "Edit" %}
</a>
</li>
<li role="presentation">
<a href="#rules-viz" role="tab" data-toggle="tab">
<span class="fa fa-eye"></span>
{% trans "Visualize" %}
</a>
</li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="rules-edit">
<checkin-rules-editor></checkin-rules-editor>
</div>
<div role="tabpanel" class="tab-pane" id="rules-viz">
<checkin-rules-visualization></checkin-rules-visualization>
</div>
</div>
</div>
<div class="alert alert-info" v-if="missingItems.length">
<p>
{% trans "Your rule always filters by product or variation, but the following products or variations are not contained in any of your rule parts so people with these tickets will not get in:" %}
</p>
<ul>
<li v-for="h in missingItems">{{ "{" }}{h}{{ "}" }}</li>
</ul>
<p>
{% trans "Please double-check if this was intentional." %}
</p>
</div>
</div> </div>
<div class="disabled-withoutjs sr-only"> <div class="disabled-withoutjs sr-only">
{{ form.rules }} {{ form.rules }}
@@ -127,11 +91,6 @@
</form> </form>
{{ items|json_script:"items" }} {{ items|json_script:"items" }}
{% if DEBUG %}
<script type="text/javascript" src="{% static "vuejs/vue.js" %}"></script>
{% else %}
<script type="text/javascript" src="{% static "vuejs/vue.min.js" %}"></script>
{% endif %}
{% compress js %} {% compress js %}
<script type="text/javascript" src="{% static "d3/d3.v6.js" %}"></script> <script type="text/javascript" src="{% static "d3/d3.v6.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-color.v2.js" %}"></script> <script type="text/javascript" src="{% static "d3/d3-color.v2.js" %}"></script>
@@ -144,15 +103,6 @@
<script type="text/javascript" src="{% static "d3/d3-drag.v2.js" %}"></script> <script type="text/javascript" src="{% static "d3/d3-drag.v2.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-zoom.v2.js" %}"></script> <script type="text/javascript" src="{% static "d3/d3-zoom.v2.js" %}"></script>
{% endcompress %} {% endcompress %}
{% compress js %} {% vite_hmr %}
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/checkinrules/jsonlogic-boolalg.js" %}"></script> {% vite_asset "src/pretix/static/pretixcontrol/js/ui/checkinrules/" %}
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/datetimefield.vue' %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/timefield.vue' %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/lookup-select2.vue' %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/checkin-rule.vue' %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/checkin-rules-editor.vue' %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/viz-node.vue' %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/checkin-rules-visualization.vue' %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/checkinrules.js" %}"></script>
{% endcompress %}
{% endblock %} {% endblock %}

View File

@@ -5,6 +5,7 @@
{% load getitem %} {% load getitem %}
{% load static %} {% load static %}
{% load compress %} {% load compress %}
{% load vite %}
{% block title %}{% trans "Check-in simulator" %}{% endblock %} {% block title %}{% trans "Check-in simulator" %}{% endblock %}
{% block inside %} {% block inside %}
<h1> <h1>
@@ -124,11 +125,9 @@
{% endif %} {% endif %}
{% if result.rule_graph %} {% if result.rule_graph %}
<div id="rules-editor" class="form-inline"> <div id="rules-editor" class="form-inline">
<div role="tabpanel" class="tab-pane" id="rules-viz"> <!-- Vue app mount point -->
<checkin-rules-visualization></checkin-rules-visualization>
</div> </div>
<textarea id="id_rules" class="sr-only">{{ result.rule_graph|attr_escapejson_dumps }}</textarea> <textarea id="id_rules" class="sr-only">{{ result.rule_graph|attr_escapejson_dumps }}</textarea>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -152,10 +151,6 @@
<script type="text/javascript" src="{% static "d3/d3-drag.v2.js" %}"></script> <script type="text/javascript" src="{% static "d3/d3-drag.v2.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-zoom.v2.js" %}"></script> <script type="text/javascript" src="{% static "d3/d3-zoom.v2.js" %}"></script>
{% endcompress %} {% endcompress %}
{% compress js %} {% vite_hmr %}
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/checkinrules/jsonlogic-boolalg.js" %}"></script> {% vite_asset "src/pretix/static/pretixcontrol/js/ui/checkinrules/" %}
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/viz-node.vue' %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/checkin-rules-visualization.vue' %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/checkinrules.js" %}"></script>
{% endcompress %}
{% endblock %} {% endblock %}

View File

@@ -1,318 +0,0 @@
$(function () {
var TYPEOPS = {
// Every change to our supported JSON logic must be done
// * in pretix.base.services.checkin
// * in pretix.base.models.checkin
// * in pretix.helpers.jsonlogic_boolalg
// * in checkinrules.js
// * in libpretixsync
// * in pretixscan-ios
'product': {
'inList': {
'label': gettext('is one of'),
'cardinality': 2,
}
},
'variation': {
'inList': {
'label': gettext('is one of'),
'cardinality': 2,
}
},
'gate': {
'inList': {
'label': gettext('is one of'),
'cardinality': 2,
}
},
'datetime': {
'isBefore': {
'label': gettext('is before'),
'cardinality': 2,
},
'isAfter': {
'label': gettext('is after'),
'cardinality': 2,
},
},
'enum_entry_status': {
'==': {
'label': gettext('='),
'cardinality': 2,
},
},
'int_by_datetime': {
'<': {
'label': '<',
'cardinality': 2,
},
'<=': {
'label': '≤',
'cardinality': 2,
},
'>': {
'label': '>',
'cardinality': 2,
},
'>=': {
'label': '≥',
'cardinality': 2,
},
'==': {
'label': '=',
'cardinality': 2,
},
'!=': {
'label': '≠',
'cardinality': 2,
},
},
'int': {
'<': {
'label': '<',
'cardinality': 2,
},
'<=': {
'label': '≤',
'cardinality': 2,
},
'>': {
'label': '>',
'cardinality': 2,
},
'>=': {
'label': '≥',
'cardinality': 2,
},
'==': {
'label': '=',
'cardinality': 2,
},
'!=': {
'label': '≠',
'cardinality': 2,
},
},
};
var VARS = {
'product': {
'label': gettext('Product'),
'type': 'product',
},
'variation': {
'label': gettext('Product variation'),
'type': 'variation',
},
'gate': {
'label': gettext('Gate'),
'type': 'gate',
},
'now': {
'label': gettext('Current date and time'),
'type': 'datetime',
},
'now_isoweekday': {
'label': gettext('Current day of the week (1 = Monday, 7 = Sunday)'),
'type': 'int',
},
'entry_status': {
'label': gettext('Current entry status'),
'type': 'enum_entry_status',
},
'entries_number': {
'label': gettext('Number of previous entries'),
'type': 'int',
},
'entries_today': {
'label': gettext('Number of previous entries since midnight'),
'type': 'int',
},
'entries_since': {
'label': gettext('Number of previous entries since'),
'type': 'int_by_datetime',
},
'entries_before': {
'label': gettext('Number of previous entries before'),
'type': 'int_by_datetime',
},
'entries_days': {
'label': gettext('Number of days with a previous entry'),
'type': 'int',
},
'entries_days_since': {
'label': gettext('Number of days with a previous entry since'),
'type': 'int_by_datetime',
},
'entries_days_before': {
'label': gettext('Number of days with a previous entry before'),
'type': 'int_by_datetime',
},
'minutes_since_last_entry': {
'label': gettext('Minutes since last entry (-1 on first entry)'),
'type': 'int',
},
'minutes_since_first_entry': {
'label': gettext('Minutes since first entry (-1 on first entry)'),
'type': 'int',
},
};
var components = {
CheckinRulesVisualization: CheckinRulesVisualization.default,
}
if (typeof CheckinRule !== "undefined") {
Vue.component('checkin-rule', CheckinRule.default);
components = {
CheckinRulesEditor: CheckinRulesEditor.default,
CheckinRulesVisualization: CheckinRulesVisualization.default,
}
}
var app = new Vue({
el: '#rules-editor',
components: components,
data: function () {
return {
rules: {},
items: [],
all_products: false,
limit_products: [],
TYPEOPS: TYPEOPS,
VARS: VARS,
texts: {
and: gettext('All of the conditions below (AND)'),
or: gettext('At least one of the conditions below (OR)'),
date_from: gettext('Event start'),
date_to: gettext('Event end'),
date_admission: gettext('Event admission'),
date_custom: gettext('custom date and time'),
date_customtime: gettext('custom time'),
date_tolerance: gettext('Tolerance (minutes)'),
condition_add: gettext('Add condition'),
minutes: gettext('minutes'),
duplicate: gettext('Duplicate'),
status_present: pgettext('entry_status', 'present'),
status_absent: pgettext('entry_status', 'absent'),
},
hasRules: false,
};
},
computed: {
missingItems: function () {
// This computed property contains list of item or variation names that
// a) Are allowed on the checkin list according to all_products or include_products
// b) Are not matched by ANY logical branch of the rule.
// The list will be empty if there is a "catch-all" rule.
var products_seen = {};
var variations_seen = {};
var rules = convert_to_dnf(this.rules);
var branch_without_product_filter = false;
if (!rules["or"]) {
rules = {"or": [rules]}
}
for (var part of rules["or"]) {
if (!part["and"]) {
part = {"and": [part]}
}
var this_branch_without_product_filter = true;
for (var subpart of part["and"]) {
if (subpart["inList"]) {
if (subpart["inList"][0]["var"] === "product" && subpart["inList"][1]) {
this_branch_without_product_filter = false;
for (var listentry of subpart["inList"][1]["objectList"]) {
products_seen[parseInt(listentry["lookup"][1])] = true
}
} else if (subpart["inList"][0]["var"] === "variation" && subpart["inList"][1]) {
this_branch_without_product_filter = false;
for (var listentry_ of subpart["inList"][1]["objectList"]) {
variations_seen[parseInt(listentry_["lookup"][1])] = true
}
}
}
}
if (this_branch_without_product_filter) {
branch_without_product_filter = true;
break;
}
}
if (branch_without_product_filter || (!Object.keys(products_seen).length && !Object.keys(variations_seen).length)) {
// At least one branch with no product filters at all that's fine.
return [];
}
var missing = [];
for (var item of this.items) {
if (products_seen[item.id]) continue;
if (!this.all_products && !this.limit_products.includes(item.id)) continue;
if (item.variations.length > 0) {
for (var variation of item.variations) {
if (variations_seen[variation.id]) continue;
missing.push(item.name + " " + variation.name)
}
} else {
missing.push(item.name)
}
}
return missing;
}
},
created: function () {
this.rules = JSON.parse($("#id_rules").val());
if ($("#items").length) {
this.items = JSON.parse($("#items").html());
var root = this.$root
function _update() {
root.all_products = $("#id_all_products").prop("checked")
root.limit_products = $("input[name=limit_products]:checked").map(function () {
return parseInt($(this).val())
}).toArray()
}
$("#id_all_products, input[name=limit_products]").on("change", function () {
_update();
})
_update()
function check_for_invalid_ids(valid_products, valid_variations, rule) {
if (rule["and"]) {
for(const child of rule["and"])
check_for_invalid_ids(valid_products, valid_variations, child);
} else if (rule["or"]) {
for(const child of rule["or"])
check_for_invalid_ids(valid_products, valid_variations, child);
} else if (rule["inList"] && rule["inList"][0]["var"] === "product") {
for(const item of rule["inList"][1]["objectList"]) {
if (!valid_products[item["lookup"][1]])
item["lookup"][2] = "[" + gettext('Error: Product not found!') + "]";
else
item["lookup"][2] = valid_products[item["lookup"][1]];
}
} else if (rule["inList"] && rule["inList"][0]["var"] === "variation") {
for(const item of rule["inList"][1]["objectList"]) {
if (!valid_variations[item["lookup"][1]])
item["lookup"][2] = "[" + gettext('Error: Variation not found!') + "]";
else
item["lookup"][2] = valid_variations[item["lookup"][1]];
}
}
}
check_for_invalid_ids(
Object.fromEntries(this.items.map(p => [p.id, p.name])),
Object.fromEntries(this.items.flatMap(p => p.variations?.map(v => [v.id, p.name + ' ' + v.name]))),
this.rules
);
}
},
watch: {
rules: {
deep: true,
handler: function (newval) {
$("#id_rules").val(JSON.stringify(newval));
}
},
}
})
});

View File

@@ -0,0 +1,98 @@
<script setup lang="ts">
import { computed } from 'vue'
import { rules as rawRules, items, allProducts, limitProducts } from './django-interop'
import { convertToDNF } from './jsonlogic-boolalg'
import RulesEditor from './checkin-rules-editor.vue'
import RulesVisualization from './checkin-rules-visualization.vue'
const gettext = (window as any).gettext
const missingItems = computed(() => {
// This computed variable contains list of item or variation names that
// a) Are allowed on the checkin list according to all_products or include_products
// b) Are not matched by ANY logical branch of the rule.
// The list will be empty if there is a "catch-all" rule.
let productsSeen = {}
let variationsSeen = {}
let rules = convertToDNF(rawRules.value)
let branchWithoutProductFilter = false
if (!rules.or) {
rules = { or: [rules] }
}
for (let part of rules.or) {
if (!part.and) {
part = { and: [part] }
}
let thisBranchWithoutProductFilter = true
for (let subpart of part.and) {
if (subpart.inList) {
if (subpart.inList[0].var === 'product' && subpart.inList[1]) {
thisBranchWithoutProductFilter = false
for (let listentry of subpart.inList[1].objectList) {
productsSeen[parseInt(listentry.lookup[1])] = true
}
} else if (subpart.inList[0].var === 'variation' && subpart.inList[1]) {
thisBranchWithoutProductFilter = false
for (let listentry_ of subpart.inList[1].objectList) {
variationsSeen[parseInt(listentry_.lookup[1])] = true
}
}
}
}
if (thisBranchWithoutProductFilter) {
branchWithoutProductFilter = true
break
}
}
if (branchWithoutProductFilter || (!Object.keys(productsSeen).length && !Object.keys(variationsSeen).length)) {
// At least one branch with no product filters at all that's fine.
return []
}
let missing = []
for (const item of items.value) {
if (productsSeen[item.id]) continue
if (!allProducts.value && !limitProducts.value.includes(item.id)) continue
if (item.variations.length > 0) {
for (let variation of item.variations) {
if (variationsSeen[variation.id]) continue
missing.push(item.name + ' ' + variation.name)
}
} else {
missing.push(item.name)
}
}
return missing
})
</script>
<template lang="pug">
#rules-editor.form-inline
div
ul.nav.nav-tabs(role="tablist")
li.active(role="presentation")
a(href="#rules-edit", role="tab", data-toggle="tab")
span.fa.fa-edit
| {{ gettext("Edit") }}
li(role="presentation")
a(href="#rules-viz", role="tab", data-toggle="tab")
span.fa.fa-eye
| {{ gettext("Visualize") }}
//- Tab panes
.tab-content
#rules-edit.tab-pane.active(v-if="items", role="tabpanel")
RulesEditor
#rules-viz.tab-pane(role="tabpanel")
RulesVisualization
.alert.alert-info(v-if="missingItems.length")
p {{ gettext("Your rule always filters by product or variation, but the following products or variations are not contained in any of your rule parts so people with these tickets will not get in:") }}
ul
li(v-for="h in missingItems", :key="h") {{ h }}
p {{ gettext("Please double-check if this was intentional.") }}
</template>
<style lang="stylus">
</style>

View File

@@ -1,355 +1,365 @@
<template> <script setup lang="ts">
<div v-bind:class="classObject"> /* eslint-disable vue/no-mutating-props */
<div class="btn-group pull-right"> import { computed } from 'vue'
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="duplicate" import { TEXTS, VARS, TYPEOPS } from './constants'
v-if="level > 0" data-toggle="tooltip" :title="texts.duplicate"> import { productSelectURL, variationSelectURL, gateSelectURL } from './django-interop'
<span class="fa fa-copy"></span> import LookupSelect2 from './lookup-select2.vue'
</button> import Datetimefield from './datetimefield.vue'
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="wrapWithOR">OR import Timefield from './timefield.vue'
</button>
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="wrapWithAND">AND
</button>
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="cutOut"
v-if="operands && operands.length === 1 && (operator === 'or' || operator === 'and')"><span
class="fa fa-cut"></span></button>
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="remove"
v-if="level > 0"><span class="fa fa-trash"></span></button>
</div>
<select v-bind:value="variable" v-on:input="setVariable" required class="form-control">
<option value="and">{{texts.and}}</option>
<option value="or">{{texts.or}}</option>
<option v-for="(v, name) in vars" :value="name">{{ v.label }}</option>
</select>
<select v-bind:value="operator" v-on:input="setOperator" required class="form-control"
v-if="operator !== 'or' && operator !== 'and' && vartype !== 'int_by_datetime'">
<option></option>
<option v-for="(v, name) in operators" :value="name">{{ v.label }}</option>
</select>
<select v-bind:value="timeType" v-on:input="setTimeType" required class="form-control"
v-if="vartype === 'datetime' || vartype === 'int_by_datetime'">
<option value="date_from">{{texts.date_from}}</option>
<option value="date_to">{{texts.date_to}}</option>
<option value="date_admission">{{texts.date_admission}}</option>
<option value="custom">{{texts.date_custom}}</option>
<option value="customtime">{{texts.date_customtime}}</option>
</select>
<datetimefield v-if="(vartype === 'datetime' || vartype === 'int_by_datetime') && timeType === 'custom'" :value="timeValue"
v-on:input="setTimeValue"></datetimefield>
<timefield v-if="(vartype === 'datetime' || vartype === 'int_by_datetime') && timeType === 'customtime'" :value="timeValue"
v-on:input="setTimeValue"></timefield>
<input class="form-control" required type="number"
v-if="vartype === 'datetime' && timeType && timeType !== 'customtime' && timeType !== 'custom'" v-bind:value="timeTolerance"
v-on:input="setTimeTolerance" :placeholder="texts.date_tolerance">
<select v-bind:value="operator" v-on:input="setOperator" required class="form-control"
v-if="vartype === 'int_by_datetime'">
<option></option>
<option v-for="(v, name) in operators" :value="name">{{ v.label }}</option>
</select>
<input class="form-control" required type="number" v-if="(vartype === 'int' || vartype === 'int_by_datetime') && cardinality > 1"
v-bind:value="rightoperand" v-on:input="setRightOperandNumber">
<lookup-select2 required v-if="vartype === 'product' && operator === 'inList'" :multiple="true"
:value="rightoperand" v-on:input="setRightOperandProductList"
:url="productSelectURL"></lookup-select2>
<lookup-select2 required v-if="vartype === 'variation' && operator === 'inList'" :multiple="true"
:value="rightoperand" v-on:input="setRightOperandVariationList"
:url="variationSelectURL"></lookup-select2>
<lookup-select2 required v-if="vartype === 'gate' && operator === 'inList'" :multiple="true"
:value="rightoperand" v-on:input="setRightOperandGateList"
:url="gateSelectURL"></lookup-select2>
<select required v-if="vartype === 'enum_entry_status' && operator === '=='"
:value="rightoperand" v-on:input="setRightOperandEnum" class="form-control">
<option value="absent">{{ texts.status_absent }}</option>
<option value="present">{{ texts.status_present }}</option>
</select>
<div class="checkin-rule-childrules" v-if="operator === 'or' || operator === 'and'">
<div v-for="(op, opi) in operands">
<checkin-rule :rule="op" :index="opi" :level="level + 1" v-if="typeof op === 'object'"></checkin-rule>
</div>
<button type="button" class="checkin-rule-addchild btn btn-xs btn-default" @click.prevent="addOperand"><span
class="fa fa-plus-circle"></span> {{ texts.condition_add }}
</button>
</div>
</div>
</template>
<script>
export default {
components: {
LookupSelect2: LookupSelect2.default,
Datetimefield: Datetimefield.default,
Timefield: Timefield.default,
},
props: {
rule: Object,
level: Number,
index: Number,
},
computed: {
texts: function () {
return this.$root.texts;
},
variable: function () {
var op = this.operator;
if (op === "and" || op === "or") {
return op;
} else if (this.rule[op] && this.rule[op][0]) {
if (this.rule[op][0]["entries_since"]) {
return "entries_since";
}
if (this.rule[op][0]["entries_before"]) {
return "entries_before";
}
if (this.rule[op][0]["entries_days_since"]) {
return "entries_days_since";
}
if (this.rule[op][0]["entries_days_before"]) {
return "entries_days_before";
}
return this.rule[op][0]["var"];
} else {
return null;
}
},
rightoperand: function () {
var op = this.operator;
if (op === "and" || op === "or") {
return null;
} else if (this.rule[op] && typeof this.rule[op][1] !== "undefined") {
return this.rule[op][1];
} else {
return null;
}
},
operator: function () {
return Object.keys(this.rule)[0];
},
operands: function () {
return this.rule[this.operator];
},
classObject: function () {
var c = {
'checkin-rule': true
};
c['checkin-rule-' + this.variable] = true;
return c;
},
vartype: function () {
if (this.variable && this.$root.VARS[this.variable]) {
return this.$root.VARS[this.variable]['type'];
}
},
timeType: function () {
if (this.vartype === 'int_by_datetime') {
if (this.rule[this.operator][0][this.variable] && this.rule[this.operator][0][this.variable][0]['buildTime']) {
return this.rule[this.operator][0][this.variable][0]['buildTime'][0];
}
} else if (this.rightoperand && this.rightoperand['buildTime']) {
return this.rightoperand['buildTime'][0];
}
},
timeTolerance: function () {
var op = this.operator;
if ((op === "isBefore" || op === "isAfter") && this.rule[op] && typeof this.rule[op][2] !== "undefined") {
return this.rule[op][2];
} else {
return null;
}
},
timeValue: function () {
if (this.vartype === 'int_by_datetime') {
if (this.rule[this.operator][0][this.variable][0]['buildTime']) {
return this.rule[this.operator][0][this.variable][0]['buildTime'][1];
}
} else if (this.rightoperand && this.rightoperand['buildTime']) {
return this.rightoperand['buildTime'][1];
}
},
cardinality: function () {
if (this.vartype && this.$root.TYPEOPS[this.vartype] && this.$root.TYPEOPS[this.vartype][this.operator]) {
return this.$root.TYPEOPS[this.vartype][this.operator]['cardinality'];
}
},
operators: function () {
return this.$root.TYPEOPS[this.vartype];
},
productSelectURL: function () {
return $("#product-select2").text();
},
variationSelectURL: function () {
return $("#variations-select2").text();
},
gateSelectURL: function () {
return $("#gates-select2").text();
},
vars: function () {
return this.$root.VARS;
},
},
methods: {
setVariable: function (event) {
var current_op = Object.keys(this.rule)[0];
var current_val = this.rule[current_op];
if (event.target.value === "and" || event.target.value === "or") { const props = defineProps<{
if (current_val[0] && current_val[0]["var"]) { rule: any
current_val = []; level: number
index: number
}>()
const emit = defineEmits<{
remove: []
duplicate: []
}>()
const operator = computed(() => Object.keys(props.rule)[0])
const operands = computed(() => props.rule[operator.value])
const variable = computed(() => {
const op = operator.value
if (op === 'and' || op === 'or') {
return op
} else if (props.rule[op]?.[0]) {
if (props.rule[op][0]['entries_since']) return 'entries_since'
if (props.rule[op][0]['entries_before']) return 'entries_before'
if (props.rule[op][0]['entries_days_since']) return 'entries_days_since'
if (props.rule[op][0]['entries_days_before']) return 'entries_days_before'
return props.rule[op][0]['var']
} }
this.$set(this.rule, event.target.value, current_val); return null
this.$delete(this.rule, current_op); })
const rightoperand = computed(() => {
const op = operator.value
if (op === 'and' || op === 'or') return null
return props.rule[op]?.[1] ?? null
})
const classObject = computed(() => ({
'checkin-rule': true,
['checkin-rule-' + variable.value]: true
}))
const vartype = computed(() => VARS[variable.value]?.type)
const timeType = computed(() => {
if (vartype.value === 'int_by_datetime') {
return props.rule[operator.value]?.[0]?.[variable.value]?.[0]?.buildTime?.[0]
}
return rightoperand.value?.buildTime?.[0]
})
const timeTolerance = computed(() => {
const op = operator.value
if ((op === 'isBefore' || op === 'isAfter') && props.rule[op]?.[2] !== undefined) {
return props.rule[op][2]
}
return null
})
const timeValue = computed(() => {
if (vartype.value === 'int_by_datetime') {
return props.rule[operator.value]?.[0]?.[variable.value]?.[0]?.buildTime?.[1]
}
return rightoperand.value?.buildTime?.[1]
})
const cardinality = computed(() => TYPEOPS[vartype.value]?.[operator.value]?.cardinality)
const operators = computed(() => TYPEOPS[vartype.value])
function setVariable (event: Event) {
const target = event.target as HTMLSelectElement
const currentOp = Object.keys(props.rule)[0]
let currentVal = props.rule[currentOp]
if (target.value === 'and' || target.value === 'or') {
if (currentVal[0]?.var) currentVal = []
props.rule[target.value] = currentVal
delete props.rule[currentOp]
} else { } else {
if (current_val !== "and" && current_val !== "or" && current_val[0] && this.$root.VARS[event.target.value]['type'] === this.vartype) { if (currentVal !== 'and' && currentVal !== 'or' && currentVal[0] && VARS[target.value]?.type === vartype.value) {
if (this.vartype === "int_by_datetime") { if (vartype.value === 'int_by_datetime') {
var current_data = this.rule[current_op][0][this.variable]; const currentData = props.rule[currentOp][0][variable.value]
var new_lhs = {}; props.rule[currentOp][0] = { [target.value]: JSON.parse(JSON.stringify(currentData)) }
new_lhs[event.target.value] = JSON.parse(JSON.stringify(current_data));
this.$set(this.rule[current_op], 0, new_lhs);
} else { } else {
this.$set(this.rule[current_op][0], "var", event.target.value); props.rule[currentOp][0].var = target.value
} }
} else if (this.$root.VARS[event.target.value]['type'] === 'int_by_datetime') { } else if (VARS[target.value]?.type === 'int_by_datetime') {
this.$delete(this.rule, current_op); delete props.rule[currentOp]
var o = {}; props.rule['!!'] = [{ [target.value]: [{ buildTime: [null, null] }] }]
o[event.target.value] = [{"buildTime": [null, null]}]
this.$set(this.rule, "!!", [o]);
} else { } else {
this.$delete(this.rule, current_op); delete props.rule[currentOp]
this.$set(this.rule, "!!", [{"var": event.target.value}]); props.rule['!!'] = [{ var: target.value }]
} }
} }
}, }
setOperator: function (event) {
var current_op = Object.keys(this.rule)[0]; function setOperator (event: Event) {
var current_val = this.rule[current_op]; const target = event.target as HTMLSelectElement
this.$delete(this.rule, current_op); const currentOp = Object.keys(props.rule)[0]
this.$set(this.rule, event.target.value, current_val); const currentVal = props.rule[currentOp]
}, delete props.rule[currentOp]
setRightOperandNumber: function (event) { props.rule[target.value] = currentVal
if (this.rule[this.operator].length === 1) { }
this.rule[this.operator].push(parseInt(event.target.value));
function setRightOperandNumber (event: Event) {
const val = parseInt((event.target as HTMLInputElement).value)
if (props.rule[operator.value].length === 1) {
props.rule[operator.value].push(val)
} else { } else {
this.$set(this.rule[this.operator], 1, parseInt(event.target.value)); props.rule[operator.value][1] = val
} }
}, }
setTimeTolerance: function (event) {
if (this.rule[this.operator].length === 2) { function setTimeTolerance (event: Event) {
this.rule[this.operator].push(parseInt(event.target.value)); const val = parseInt((event.target as HTMLInputElement).value)
if (props.rule[operator.value].length === 2) {
props.rule[operator.value].push(val)
} else { } else {
this.$set(this.rule[this.operator], 2, parseInt(event.target.value)); props.rule[operator.value][2] = val
} }
}, }
setTimeType: function (event) {
var time = { function setTimeType (event: Event) {
"buildTime": [event.target.value] const val = (event.target as HTMLSelectElement).value
}; const time = { buildTime: [val] }
if (this.vartype === "int_by_datetime") { if (vartype.value === 'int_by_datetime') {
this.$set(this.rule[this.operator][0][this.variable], 0, time); props.rule[operator.value][0][variable.value][0] = time
} else { } else {
if (this.rule[this.operator].length === 1) { if (props.rule[operator.value].length === 1) {
this.rule[this.operator].push(time); props.rule[operator.value].push(time)
} else { } else {
this.$set(this.rule[this.operator], 1, time); props.rule[operator.value][1] = time
} }
if (event.target.value === "custom") { if (val === 'custom') {
this.$set(this.rule[this.operator], 2, 0); props.rule[operator.value][2] = 0
} }
} }
}, }
setTimeValue: function (val) {
if (this.vartype === "int_by_datetime") { function setTimeValue (val: string) {
this.$set(this.rule[this.operator][0][this.variable][0]["buildTime"], 1, val); if (vartype.value === 'int_by_datetime') {
props.rule[operator.value][0][variable.value][0]['buildTime'][1] = val
} else { } else {
this.$set(this.rule[this.operator][1]["buildTime"], 1, val); props.rule[operator.value][1]['buildTime'][1] = val
} }
},
setRightOperandProductList: function (val) {
var products = {
"objectList": []
};
for (var i = 0; i < val.length; i++) {
products["objectList"].push({
"lookup": [
"product",
val[i].id,
val[i].text
]
});
} }
if (this.rule[this.operator].length === 1) {
this.rule[this.operator].push(products); function setRightOperandProductList (val: { id: any; text: string }[]) {
const products = { objectList: val.map(item => ({ lookup: ['product', item.id, item.text] })) }
if (props.rule[operator.value].length === 1) {
props.rule[operator.value].push(products)
} else { } else {
this.$set(this.rule[this.operator], 1, products); props.rule[operator.value][1] = products
} }
},
setRightOperandVariationList: function (val) {
var products = {
"objectList": []
};
for (var i = 0; i < val.length; i++) {
products["objectList"].push({
"lookup": [
"variation",
val[i].id,
val[i].text
]
});
} }
if (this.rule[this.operator].length === 1) {
this.rule[this.operator].push(products); function setRightOperandVariationList (val: { id: any; text: string }[]) {
const products = { objectList: val.map(item => ({ lookup: ['variation', item.id, item.text] })) }
if (props.rule[operator.value].length === 1) {
props.rule[operator.value].push(products)
} else { } else {
this.$set(this.rule[this.operator], 1, products); props.rule[operator.value][1] = products
} }
},
setRightOperandGateList: function (val) {
var products = {
"objectList": []
};
for (var i = 0; i < val.length; i++) {
products["objectList"].push({
"lookup": [
"gate",
val[i].id,
val[i].text
]
});
} }
if (this.rule[this.operator].length === 1) {
this.rule[this.operator].push(products); function setRightOperandGateList (val: { id: any; text: string }[]) {
const products = { objectList: val.map(item => ({ lookup: ['gate', item.id, item.text] })) }
if (props.rule[operator.value].length === 1) {
props.rule[operator.value].push(products)
} else { } else {
this.$set(this.rule[this.operator], 1, products); props.rule[operator.value][1] = products
} }
}, }
setRightOperandEnum: function (event) {
if (this.rule[this.operator].length === 1) { function setRightOperandEnum (event: Event) {
this.rule[this.operator].push(event.target.value); const val = (event.target as HTMLSelectElement).value
if (props.rule[operator.value].length === 1) {
props.rule[operator.value].push(val)
} else { } else {
this.$set(this.rule[this.operator], 1, event.target.value); props.rule[operator.value][1] = val
} }
},
addOperand: function () {
this.rule[this.operator].push({"": []});
},
wrapWithOR: function () {
var r = JSON.parse(JSON.stringify(this.rule));
this.$delete(this.rule, this.operator);
this.$set(this.rule, "or", [r]);
},
wrapWithAND: function () {
var r = JSON.parse(JSON.stringify(this.rule));
this.$delete(this.rule, this.operator);
this.$set(this.rule, "and", [r]);
},
cutOut: function () {
var cop = Object.keys(this.operands[0])[0];
var r = this.operands[0][cop];
this.$delete(this.rule, this.operator);
this.$set(this.rule, cop, r);
},
remove: function () {
this.$parent.rule[this.$parent.operator].splice(this.index, 1);
},
duplicate: function () {
var r = JSON.parse(JSON.stringify(this.rule));
this.$parent.rule[this.$parent.operator].splice(this.index, 0, r);
},
} }
function addOperand () {
props.rule[operator.value].push({ '': [] })
}
function wrapWithOR () {
const r = JSON.parse(JSON.stringify(props.rule))
delete props.rule[operator.value]
props.rule.or = [r]
}
function wrapWithAND () {
const r = JSON.parse(JSON.stringify(props.rule))
delete props.rule[operator.value]
props.rule.and = [r]
}
function cutOut () {
const cop = Object.keys(operands.value[0])[0]
const r = operands.value[0][cop]
delete props.rule[operator.value]
props.rule[cop] = r
}
function remove () {
emit('remove')
}
function duplicate () {
emit('duplicate')
}
function removeChild (index: number) {
props.rule[operator.value].splice(index, 1)
}
function duplicateChild (index: number) {
const r = JSON.parse(JSON.stringify(props.rule[operator.value][index]))
props.rule[operator.value].splice(index, 0, r)
} }
</script> </script>
<template lang="pug">
div(:class="classObject")
.btn-group.pull-right
button.checkin-rule-remove.btn.btn-xs.btn-default(
v-if="level > 0",
type="button",
data-toggle="tooltip",
:title="TEXTS.duplicate",
@click.prevent="duplicate"
)
span.fa.fa-copy
button.checkin-rule-remove.btn.btn-xs.btn-default(
type="button",
@click.prevent="wrapWithOR"
) OR
button.checkin-rule-remove.btn.btn-xs.btn-default(
type="button",
@click.prevent="wrapWithAND"
) AND
button.checkin-rule-remove.btn.btn-xs.btn-default(
v-if="operands && operands.length === 1 && (operator === 'or' || operator === 'and')",
type="button",
@click.prevent="cutOut"
)
span.fa.fa-cut
button.checkin-rule-remove.btn.btn-xs.btn-default(
v-if="level > 0",
type="button",
@click.prevent="remove"
)
span.fa.fa-trash
select.form-control(:value="variable", required, @input="setVariable")
option(value="and") {{ TEXTS.and }}
option(value="or") {{ TEXTS.or }}
option(v-for="(v, name) in VARS", :key="name", :value="name") {{ v.label }}
select.form-control(
v-if="operator !== 'or' && operator !== 'and' && vartype !== 'int_by_datetime'",
:value="operator",
required,
@input="setOperator"
)
option
option(v-for="(v, name) in operators", :key="name", :value="name") {{ v.label }}
select.form-control(
v-if="vartype === 'datetime' || vartype === 'int_by_datetime'",
:value="timeType",
required,
@input="setTimeType"
)
option(value="date_from") {{ TEXTS.date_from }}
option(value="date_to") {{ TEXTS.date_to }}
option(value="date_admission") {{ TEXTS.date_admission }}
option(value="custom") {{ TEXTS.date_custom }}
option(value="customtime") {{ TEXTS.date_customtime }}
Datetimefield(
v-if="(vartype === 'datetime' || vartype === 'int_by_datetime') && timeType === 'custom'",
:value="timeValue",
@input="setTimeValue"
)
Timefield(
v-if="(vartype === 'datetime' || vartype === 'int_by_datetime') && timeType === 'customtime'",
:value="timeValue",
@input="setTimeValue"
)
input.form-control(
v-if="vartype === 'datetime' && timeType && timeType !== 'customtime' && timeType !== 'custom'",
required,
type="number",
:value="timeTolerance",
:placeholder="TEXTS.date_tolerance",
@input="setTimeTolerance"
)
select.form-control(
v-if="vartype === 'int_by_datetime'",
:value="operator",
required,
@input="setOperator"
)
option
option(v-for="(v, name) in operators", :key="name", :value="name") {{ v.label }}
input.form-control(
v-if="(vartype === 'int' || vartype === 'int_by_datetime') && cardinality > 1",
required,
type="number",
:value="rightoperand",
@input="setRightOperandNumber"
)
LookupSelect2(
v-if="vartype === 'product' && operator === 'inList'",
required,
:multiple="true",
:value="rightoperand",
:url="productSelectURL",
@input="setRightOperandProductList"
)
LookupSelect2(
v-if="vartype === 'variation' && operator === 'inList'",
required,
:multiple="true",
:value="rightoperand",
:url="variationSelectURL",
@input="setRightOperandVariationList"
)
LookupSelect2(
v-if="vartype === 'gate' && operator === 'inList'",
required,
:multiple="true",
:value="rightoperand",
:url="gateSelectURL",
@input="setRightOperandGateList"
)
select.form-control(
v-if="vartype === 'enum_entry_status' && operator === '=='",
required,
:value="rightoperand",
@input="setRightOperandEnum"
)
option(value="absent") {{ TEXTS.status_absent }}
option(value="present") {{ TEXTS.status_present }}
.checkin-rule-childrules(v-if="operator === 'or' || operator === 'and'")
div(v-for="(op, opi) in operands", :key="opi")
CheckinRule(
v-if="typeof op === 'object'",
:rule="op",
:index="opi",
:level="level + 1",
@remove="removeChild(opi)",
@duplicate="duplicateChild(opi)"
)
button.checkin-rule-addchild.btn.btn-xs.btn-default(
type="button",
@click.prevent="addOperand"
)
span.fa.fa-plus-circle
| {{ TEXTS.condition_add }}
</template>

View File

@@ -1,25 +1,23 @@
<template> <script setup lang="ts">
<div class="checkin-rules-editor"> import { computed } from 'vue'
<checkin-rule :rule="this.$root.rules" :level="0" :index="0" v-if="hasRules"></checkin-rule> import { TEXTS } from './constants'
<button type="button" class="checkin-rule-addchild btn btn-xs btn-default" v-if="!hasRules" import { rules } from './django-interop'
@click.prevent="addRule"><span class="fa fa-plus-circle"></span> {{ this.$root.texts.condition_add }} import CheckinRule from './checkin-rule.vue'
</button>
</div> const hasRules = computed(() => !!Object.keys(rules.value).length)
</template>
<script> function addRule () {
export default { rules.value.and = []
components: {
CheckinRule: CheckinRule.default,
},
computed: {
hasRules: function () {
return !!Object.keys(this.$root.rules).length;
}
},
methods: {
addRule: function () {
this.$set(this.$root.rules, "and", []);
},
},
} }
</script> </script>
<template lang="pug">
.checkin-rules-editor
CheckinRule(v-if="hasRules", :rule="rules", :level="0", :index="0")
button.checkin-rule-addchild.btn.btn-xs.btn-default(
v-if="!hasRules",
type="button",
@click.prevent="addRule"
)
span.fa.fa-plus-circle
| {{ TEXTS.condition_add }}
</template>

View File

@@ -1,51 +1,39 @@
<template> <script setup lang="ts">
<div :class="'checkin-rules-visualization ' + (maximized ? 'maximized' : '')"> import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
<div class="tools"> import { rules } from './django-interop'
<button v-if="maximized" class="btn btn-default" type="button" @click.prevent="maximized = false"><span class="fa fa-window-close"></span></button> import VizNode from './viz-node.vue'
<button v-if="!maximized" class="btn btn-default" type="button" @click.prevent="maximized = true"><span class="fa fa-window-maximize"></span></button>
</div> declare const d3: any
<svg :width="graph.columns * (boxWidth + marginX) + 2 * paddingX" :height="graph.height * (boxHeight + marginY)"
:viewBox="viewBox" ref="svg"> const svg = ref<SVGSVGElement | null>(null)
<g :transform="zoomTransform.toString()"> const maximized = ref(false)
<viz-node v-for="(node, nodeid) in graph.nodes_by_id" :key="nodeid" :node="node" const zoom = ref<any>(null)
:children="node.children.map(n => graph.nodes_by_id[n])" :nodeid="nodeid" const defaultScale = ref(1)
:boxWidth="boxWidth" :boxHeight="boxHeight" :marginX="marginX" :marginY="marginY" const zoomTransform = ref(d3.zoomTransform({ k: 1, x: 0, y: 0 }))
:paddingX="paddingX"></viz-node>
</g> const boxWidth = 300
</svg> const boxHeight = 62
</div> const paddingX = 50
</template> const marginX = 50
<script> const marginY = 20
export default {
components: { interface GraphNode {
VizNode: VizNode.default, rule: any
}, column: number
computed: { children: string[]
boxWidth() { y?: number
return 300 parent?: GraphNode
}, }
boxHeight() {
return 62 interface Graph {
}, nodes_by_id: Record<string, GraphNode>
paddingX() { children: string[]
return 50 columns: number
}, height: number
marginX() { y?: number
return 50 }
},
marginY() { const graph = computed<Graph>(() => {
return 20
},
contentWidth() {
return this.graph.columns * (this.boxWidth + this.marginX) + 2 * this.paddingX
},
contentHeight() {
return this.graph.height * (this.boxHeight + this.marginY)
},
viewBox() {
return `0 0 ${this.contentWidth} ${this.contentHeight}`
},
graph() {
/** /**
* Converts a JSON logic rule into a "flow chart". * Converts a JSON logic rule into a "flow chart".
* *
@@ -76,18 +64,19 @@ export default {
* \ / * \ /
* --- D --- * --- D ---
*/ */
const graph = { const graphData: Graph = {
nodes_by_id: {}, nodes_by_id: {},
children: [], children: [],
columns: -1, columns: -1,
height: 1,
} }
// Step 1: Start building the graph by finding all nodes and edges // Step 1: Start building the graph by finding all nodes and edges
let counter = 0; let counter = 0
const _add_to_graph = (rule) => { // returns [heads, tails] const _add_to_graph = (rule: any): [string[], string[]] => { // returns [heads, tails]
if (typeof rule !== 'object' || rule === null) { if (typeof rule !== 'object' || rule === null) {
const node_id = (counter++).toString() const node_id = (counter++).toString()
graph.nodes_by_id[node_id] = { graphData.nodes_by_id[node_id] = {
rule: rule, rule: rule,
column: -1, column: -1,
children: [], children: [],
@@ -98,16 +87,16 @@ export default {
const operator = Object.keys(rule)[0] const operator = Object.keys(rule)[0]
const operands = rule[operator] const operands = rule[operator]
if (operator === "and") { if (operator === 'and') {
let children = [] let children: string[] = []
let tails = null let tails: string[] | null = null
operands.reverse() operands.reverse()
for (let operand of operands) { for (const operand of operands) {
let [new_children, new_tails] = _add_to_graph(operand) const [new_children, new_tails] = _add_to_graph(operand)
for (let new_child of new_tails) { for (const new_child of new_tails) {
graph.nodes_by_id[new_child].children.push(...children) graphData.nodes_by_id[new_child].children.push(...children)
for (let c of children) { for (const c of children) {
graph.nodes_by_id[c].parent = graph.nodes_by_id[new_child] graphData.nodes_by_id[c].parent = graphData.nodes_by_id[new_child]
} }
} }
if (tails === null) { if (tails === null) {
@@ -115,141 +104,173 @@ export default {
} }
children = new_children children = new_children
} }
return [children, tails] return [children, tails!]
} else if (operator === "or") { } else if (operator === 'or') {
const children = [] const children: string[] = []
const tails = [] const tails: string[] = []
for (let operand of operands) { for (const operand of operands) {
let [new_children, new_tails] = _add_to_graph(operand) const [new_children, new_tails] = _add_to_graph(operand)
children.push(...new_children) children.push(...new_children)
tails.push(...new_tails) tails.push(...new_tails)
} }
return [children, tails] return [children, tails]
} else { } else {
const node_id = (counter++).toString() const node_id = (counter++).toString()
graph.nodes_by_id[node_id] = { graphData.nodes_by_id[node_id] = {
rule: rule, rule: rule,
column: -1, column: -1,
children: [], children: [],
} }
return [[node_id], [node_id]] return [[node_id], [node_id]]
} }
} }
graph.children = _add_to_graph(JSON.parse(JSON.stringify(this.$root.rules)))[0] graphData.children = _add_to_graph(JSON.parse(JSON.stringify(rules.value)))[0]
// Step 2: We compute the "column" of every node, which is the maximum number of hops required to reach the // Step 2: We compute the "column" of every node, which is the maximum number of hops required to reach the
// node from the root node // node from the root node
const _set_column_to_min = (nodes, mincol) => { const _set_column_to_min = (nodes: GraphNode[], mincol: number) => {
for (let node of nodes) { for (const node of nodes) {
if (mincol > node.column) { if (mincol > node.column) {
node.column = mincol node.column = mincol
graph.columns = Math.max(mincol + 1, graph.columns) graphData.columns = Math.max(mincol + 1, graphData.columns)
_set_column_to_min(node.children.map(nid => graph.nodes_by_id[nid]), mincol + 1) _set_column_to_min(node.children.map(nid => graphData.nodes_by_id[nid]), mincol + 1)
} }
} }
} }
_set_column_to_min(graph.children.map(nid => graph.nodes_by_id[nid]), 0) _set_column_to_min(graphData.children.map(nid => graphData.nodes_by_id[nid]), 0)
// Step 3: Align each node on a grid. The x position is already given by the column computed above, but we still // Step 3: Align each node on a grid. The x position is already given by the column computed above, but we still
// need the y position. This part of the algorithm is opinionated and probably not yet the nicest solution we // need the y position. This part of the algorithm is opinionated and probably not yet the nicest solution we
// can use! // can use!
const _set_y = (node, offset) => { const _set_y = (node: Graph | GraphNode, offset: number): number => {
if (typeof node.y === "undefined") { if (typeof node.y === 'undefined') {
// We only take the first value we found for each node // We only take the first value we found for each node
node.y = offset node.y = offset
} }
let used = 0 let used = 0
for (let cid of node.children) { for (const cid of node.children) {
used += Math.max(0, _set_y(graph.nodes_by_id[cid], offset + used) - 1) used += Math.max(0, _set_y(graphData.nodes_by_id[cid], offset + used) - 1)
used++ used++
} }
return used return used
} }
_set_y(graph, 0) _set_y(graphData, 0)
// Step 4: Compute the "height" of the graph by looking at the node with the highest y value // Step 4: Compute the "height" of the graph by looking at the node with the highest y value
graph.height = 1 graphData.height = 1
for (let node of [...Object.values(graph.nodes_by_id)]) { for (const node of [...Object.values(graphData.nodes_by_id)]) {
graph.height = Math.max(graph.height, node.y + 1) graphData.height = Math.max(graphData.height, (node.y ?? 0) + 1)
} }
return graph return graphData
}
},
mounted() {
this.createZoom()
},
created() {
window.addEventListener('resize', this.createZoom)
},
destroyed() {
window.removeEventListener('resize', this.createZoom)
},
watch: {
maximized() {
this.$nextTick(() => {
this.createZoom()
}) })
}
},
methods: {
createZoom() {
if (!this.$refs.svg) return
const viewportHeight = this.$refs.svg.clientHeight const contentWidth = computed(() => {
const viewportWidth = this.$refs.svg.clientWidth return graph.value.columns * (boxWidth + marginX) + 2 * paddingX
this.defaultScale = 1 })
this.zoom = d3 const contentHeight = computed(() => {
return graph.value.height * (boxHeight + marginY)
})
const viewBox = computed(() => {
return `0 0 ${contentWidth.value} ${contentHeight.value}`
})
function createZoom () {
if (!svg.value) return
const viewportHeight = svg.value.clientHeight
const viewportWidth = svg.value.clientWidth
defaultScale.value = 1
zoom.value = d3
.zoom() .zoom()
.scaleExtent([Math.min(this.defaultScale * 0.5, 1), Math.max(5, this.contentHeight / viewportHeight, this.contentWidth / viewportWidth)]) .scaleExtent([Math.min(defaultScale.value * 0.5, 1), Math.max(5, contentHeight.value / viewportHeight, contentWidth.value / viewportWidth)])
.extent([[0, 0], [viewportWidth, viewportHeight]]) .extent([[0, 0], [viewportWidth, viewportHeight]])
.filter(event => { .filter((event: any) => {
const wheeled = event.type === 'wheel' const wheeled = event.type === 'wheel'
const mouseDrag = const mouseDrag
event.type === 'mousedown' || = event.type === 'mousedown'
event.type === 'mouseup' || || event.type === 'mouseup'
event.type === 'mousemove' || event.type === 'mousemove'
const touch = const touch
event.type === 'touchstart' || = event.type === 'touchstart'
event.type === 'touchmove' || || event.type === 'touchmove'
event.type === 'touchstop' || event.type === 'touchstop'
return (wheeled || mouseDrag || touch) && this.maximized return (wheeled || mouseDrag || touch) && maximized.value
}) })
.wheelDelta(event => { .wheelDelta((event: any) => {
// In contrast to default implementation, do not use a factor 10 if ctrl is pressed // In contrast to default implementation, do not use a factor 10 if ctrl is pressed
return -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) return -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002)
}) })
.on('zoom', (event) => { .on('zoom', (event: any) => {
this.zoomTransform = event.transform zoomTransform.value = event.transform
}) })
const initTransform = d3.zoomIdentity const initTransform = d3.zoomIdentity
.scale(this.defaultScale) .scale(defaultScale.value)
.translate( .translate(0, 0)
0, zoomTransform.value = initTransform
0
)
this.zoomTransform = initTransform
// This sets correct d3 internal state for the initial centering // This sets correct d3 internal state for the initial centering
d3.select(this.$refs.svg) d3.select(svg.value)
.call(this.zoom.transform, initTransform) .call(zoom.value.transform, initTransform)
const svg = d3.select(this.$refs.svg).call(this.zoom) const svgSelection = d3.select(svg.value).call(zoom.value)
svg.on('touchmove.zoom', null) svgSelection.on('touchmove.zoom', null)
// TODO touch support // TODO touch support
},
},
data() {
return {
maximized: false,
zoom: null,
defaultScale: 1,
zoomTransform: d3.zoomTransform({k: 1, x: 0, y: 0}),
}
}
} }
watch(maximized, () => {
nextTick(() => {
createZoom()
})
})
onMounted(() => {
createZoom()
window.addEventListener('resize', createZoom)
})
onUnmounted(() => {
window.removeEventListener('resize', createZoom)
})
</script> </script>
<template lang="pug">
div(:class="'checkin-rules-visualization ' + (maximized ? 'maximized' : '')")
.tools
button.btn.btn-default(
v-if="maximized",
type="button",
@click.prevent="maximized = false"
)
span.fa.fa-window-close
button.btn.btn-default(
v-if="!maximized",
type="button",
@click.prevent="maximized = true"
)
span.fa.fa-window-maximize
svg(
ref="svg",
:width="contentWidth",
:height="contentHeight",
:viewBox="viewBox"
)
g(:transform="zoomTransform.toString()")
VizNode(
v-for="(node, nodeid) in graph.nodes_by_id",
:key="nodeid",
:node="node",
:children="node.children.map((n: string) => graph.nodes_by_id[n])",
:nodeid="nodeid",
:boxWidth="boxWidth",
:boxHeight="boxHeight",
:marginX="marginX",
:marginY="marginY",
:paddingX="paddingX"
)
</template>

View File

@@ -0,0 +1,193 @@
/* global gettext, pgettext */
export const TEXTS = {
and: gettext('All of the conditions below (AND)'),
or: gettext('At least one of the conditions below (OR)'),
date_from: gettext('Event start'),
date_to: gettext('Event end'),
date_admission: gettext('Event admission'),
date_custom: gettext('custom date and time'),
date_customtime: gettext('custom time'),
date_tolerance: gettext('Tolerance (minutes)'),
condition_add: gettext('Add condition'),
minutes: gettext('minutes'),
duplicate: gettext('Duplicate'),
status_present: pgettext('entry_status', 'present'),
status_absent: pgettext('entry_status', 'absent'),
}
export const TYPEOPS = {
// Every change to our supported JSON logic must be done
// * in pretix.base.services.checkin
// * in pretix.base.models.checkin
// * in pretix.helpers.jsonlogic_boolalg
// * in checkinrules.js
// * in libpretixsync
// * in pretixscan-ios
product: {
inList: {
label: gettext('is one of'),
cardinality: 2,
}
},
variation: {
inList: {
label: gettext('is one of'),
cardinality: 2,
}
},
gate: {
inList: {
label: gettext('is one of'),
cardinality: 2,
}
},
datetime: {
isBefore: {
label: gettext('is before'),
cardinality: 2,
},
isAfter: {
label: gettext('is after'),
cardinality: 2,
},
},
enum_entry_status: {
'==': {
label: gettext('='),
cardinality: 2,
},
},
int_by_datetime: {
'<': {
label: '<',
cardinality: 2,
},
'<=': {
label: '≤',
cardinality: 2,
},
'>': {
label: '>',
cardinality: 2,
},
'>=': {
label: '≥',
cardinality: 2,
},
'==': {
label: '=',
cardinality: 2,
},
'!=': {
label: '≠',
cardinality: 2,
},
},
int: {
'<': {
label: '<',
cardinality: 2,
},
'<=': {
label: '≤',
cardinality: 2,
},
'>': {
label: '>',
cardinality: 2,
},
'>=': {
label: '≥',
cardinality: 2,
},
'==': {
label: '=',
cardinality: 2,
},
'!=': {
label: '≠',
cardinality: 2,
},
},
}
export const VARS = {
product: {
label: gettext('Product'),
type: 'product',
},
variation: {
label: gettext('Product variation'),
type: 'variation',
},
gate: {
label: gettext('Gate'),
type: 'gate',
},
now: {
label: gettext('Current date and time'),
type: 'datetime',
},
now_isoweekday: {
label: gettext('Current day of the week (1 = Monday, 7 = Sunday)'),
type: 'int',
},
entry_status: {
label: gettext('Current entry status'),
type: 'enum_entry_status',
},
entries_number: {
label: gettext('Number of previous entries'),
type: 'int',
},
entries_today: {
label: gettext('Number of previous entries since midnight'),
type: 'int',
},
entries_since: {
label: gettext('Number of previous entries since'),
type: 'int_by_datetime',
},
entries_before: {
label: gettext('Number of previous entries before'),
type: 'int_by_datetime',
},
entries_days: {
label: gettext('Number of days with a previous entry'),
type: 'int',
},
entries_days_since: {
label: gettext('Number of days with a previous entry since'),
type: 'int_by_datetime',
},
entries_days_before: {
label: gettext('Number of days with a previous entry before'),
type: 'int_by_datetime',
},
minutes_since_last_entry: {
label: gettext('Minutes since last entry (-1 on first entry)'),
type: 'int',
},
minutes_since_first_entry: {
label: gettext('Minutes since first entry (-1 on first entry)'),
type: 'int',
},
}
export const DATETIME_OPTIONS = {
format: document.body.dataset.datetimeformat,
locale: document.body.dataset.datetimelocale,
useCurrent: false,
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove'
}
}

View File

@@ -1,55 +1,45 @@
<template> <script setup lang="ts">
<input class="form-control"> import { ref, watch, onMounted, onUnmounted } from 'vue'
</template> import { DATETIME_OPTIONS } from './constants'
<script>
export default { const props = defineProps<{
props: ["required", "value"], required?: boolean
template: (''), value?: string
mounted: function () { }>()
var vm = this;
var multiple = this.multiple; const emit = defineEmits<{
$(this.$el) input: [value: string]
.datetimepicker(this.opts()) }>()
.trigger("change")
.on("dp.change", function (e) { const input = ref<HTMLInputElement | null>(null)
vm.$emit("input", $(this).data('DateTimePicker').date().toISOString());
}); watch(() => props.value, (val) => {
if (!vm.value) { $(input.value).data('DateTimePicker').date(moment(val))
$(this.$el).data("DateTimePicker").viewDate(moment().hour(0).minute(0).second(0).millisecond(0)); })
onMounted(() => {
$(input.value)
.datetimepicker({
...DATETIME_OPTIONS,
showClear: props.required,
})
.trigger('change')
.on('dp.change', function (this: HTMLElement) {
emit('input', $(this).data('DateTimePicker').date().toISOString())
})
if (!props.value) {
$(input.value).data('DateTimePicker').viewDate(moment().hour(0).minute(0).second(0).millisecond(0))
} else { } else {
$(this.$el).data("DateTimePicker").date(moment(vm.value)); $(input.value).data('DateTimePicker').date(moment(props.value))
} }
}, })
methods: {
opts: function () { onUnmounted(() => {
return { $(input.value)
format: $("body").attr("data-datetimeformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: this.required,
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove'
}
};
}
},
watch: {
value: function (val) {
$(this.$el).data('DateTimePicker').date(moment(val));
},
},
destroyed: function () {
$(this.$el)
.off() .off()
.datetimepicker("destroy"); .datetimepicker('destroy')
} })
}
</script> </script>
<template lang="pug">
input.form-control(ref="input")
</template>

View File

@@ -0,0 +1,68 @@
import { ref, watch } from 'vue'
export const allProducts = ref(false)
export const limitProducts = ref<number[]>([])
function updateProducts () {
allProducts.value = document.querySelector<HTMLInputElement>('#id_all_products')?.checked ?? false
limitProducts.value = Array.from(document.querySelectorAll<HTMLInputElement>('input[name=limit_products]:checked')).map(el => parseInt(el.value))
}
// listen to change events for products
document.querySelectorAll('#id_all_products, input[name=limit_products]').forEach(el => el.addEventListener('change', updateProducts))
updateProducts()
export const rules = ref<any>({})
// grab rules from hidden input
const rulesInput = document.querySelector<HTMLInputElement>('#id_rules')
if (rulesInput?.value) {
rules.value = JSON.parse(rulesInput.value)
}
// sync back to hidden input
watch(rules, (newVal) => {
if (!rulesInput) return
rulesInput.value = JSON.stringify(newVal)
}, { deep: true })
export const items = ref<any[]>([])
const itemsEl = document.querySelector('#items')
if (itemsEl?.textContent) {
items.value = JSON.parse(itemsEl.textContent || '[]')
function checkForInvalidIds (validProducts: Record<string, string>, validVariations: Record<string, string>, rule: any) {
if (rule['and']) {
for (const child of rule['and'])
checkForInvalidIds(validProducts, validVariations, child)
} else if (rule['or']) {
for (const child of rule['or'])
checkForInvalidIds(validProducts, validVariations, child)
} else if (rule['inList'] && rule['inList'][0]['var'] === 'product') {
for (const item of rule['inList'][1]['objectList']) {
if (!validProducts[item['lookup'][1]])
item['lookup'][2] = '[' + gettext('Error: Product not found!') + ']'
else
item['lookup'][2] = validProducts[item['lookup'][1]]
}
} else if (rule['inList'] && rule['inList'][0]['var'] === 'variation') {
for (const item of rule['inList'][1]['objectList']) {
if (!validVariations[item['lookup'][1]])
item['lookup'][2] = '[' + gettext('Error: Variation not found!') + ']'
else
item['lookup'][2] = validVariations[item['lookup'][1]]
}
}
}
checkForInvalidIds(
Object.fromEntries(items.value.map(p => [p.id, p.name])),
Object.fromEntries(items.value.flatMap(p => p.variations?.map(v => [v.id, p.name + ' ' + v.name]) ?? [])),
rules.value
)
}
export const productSelectURL = ref(document.querySelector('#product-select2')?.textContent)
export const variationSelectURL = ref(document.querySelector('#variations-select2')?.textContent)
export const gateSelectURL = ref(document.querySelector('#gate-select2')?.textContent)

View File

@@ -0,0 +1,12 @@
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#rules-editor')
app.config.errorHandler = (error, _vm, info) => {
// vue fatals on errors by default, which is a weird choice
// https://github.com/vuejs/core/issues/3525
// https://github.com/vuejs/router/discussions/2435
console.error('[VUE]', info, error)
}

View File

@@ -1,19 +1,19 @@
function convert_to_dnf(rules) { export function convertToDNF (rules) {
// Converts a set of rules to disjunctive normal form, i.e. returns something of the form // Converts a set of rules to disjunctive normal form, i.e. returns something of the form
// `(a AND b AND c) OR (a AND d AND f)` // `(a AND b AND c) OR (a AND d AND f)`
// without further nesting. // without further nesting.
if (typeof rules !== "object" || Array.isArray(rules) || rules === null) { if (typeof rules !== 'object' || Array.isArray(rules) || rules === null) {
return rules return rules
} }
function _distribute_or_over_and (r) { function _distribute_or_over_and (r) {
var operator = Object.keys(r)[0] let operator = Object.keys(r)[0]
var values = r[operator] let values = r[operator]
if (operator === "and") { if (operator === 'and') {
var arg_to_distribute = null let arg_to_distribute = null
var other_args = [] let other_args = []
for (var arg of values) { for (let arg of values) {
if (typeof arg === "object" && !Array.isArray(arg) && typeof arg["or"] !== "undefined" && arg_to_distribute === null) { if (typeof arg === 'object' && !Array.isArray(arg) && typeof arg['or'] !== 'undefined' && arg_to_distribute === null) {
arg_to_distribute = arg arg_to_distribute = arg
} else { } else {
other_args.push(arg) other_args.push(arg)
@@ -22,17 +22,17 @@ function convert_to_dnf(rules) {
if (arg_to_distribute === null) { if (arg_to_distribute === null) {
return r return r
} }
var or_operands = [] let or_operands = []
for (var dval of arg_to_distribute["or"]) { for (let dval of arg_to_distribute['or']) {
or_operands.push({"and": other_args.concat([dval])}) or_operands.push({ and: other_args.concat([dval]) })
} }
return { return {
"or": or_operands or: or_operands
} }
} else if (!operator) { } else if (!operator) {
return r return r
} else if (operator === "!" || operator === "!!" || operator === "?:" || operator === "if") { } else if (operator === '!' || operator === '!!' || operator === '?:' || operator === 'if') {
console.warn("Operator " + operator + " currently unsupported by convert_to_dnf") console.warn('Operator ' + operator + ' currently unsupported by convert_to_dnf')
return r return r
} else { } else {
return r return r
@@ -41,35 +41,35 @@ function convert_to_dnf(rules) {
function _simplify_chained_operators (r) { function _simplify_chained_operators (r) {
// Simplify `(a OR b) OR (c or d)` to `a OR b OR c OR d` and the same with `AND` // Simplify `(a OR b) OR (c or d)` to `a OR b OR c OR d` and the same with `AND`
if (typeof r !== "object" || Array.isArray(r)) { if (typeof r !== 'object' || Array.isArray(r)) {
return r return r
} }
var operator = Object.keys(r)[0] let operator = Object.keys(r)[0]
var values = r[operator] let values = r[operator]
if (operator !== "or" && operator !== "and") { if (operator !== 'or' && operator !== 'and') {
return r return r
} }
var new_values = [] let new_values = []
for (var v of values) { for (let v of values) {
if (typeof v !== "object" || Array.isArray(v) || typeof v[operator] === "undefined") { if (typeof v !== 'object' || Array.isArray(v) || typeof v[operator] === 'undefined') {
new_values.push(v) new_values.push(v)
} else { } else {
new_values.push(...v[operator]) new_values.push(...v[operator])
} }
} }
var result = {} let result = {}
result[operator] = new_values result[operator] = new_values
return result return result
} }
// Run _distribute_or_over_and on until it no longer changes anything. Do so recursively // Run _distribute_or_over_and on until it no longer changes anything. Do so recursively
// for the full expression tree. // for the full expression tree.
var old_rules = rules let old_rules = rules
while (true) { while (true) {
rules = _distribute_or_over_and(rules) rules = _distribute_or_over_and(rules)
var operator = Object.keys(rules)[0] let operator = Object.keys(rules)[0]
var values = rules[operator] let values = rules[operator]
var no_list = false let no_list = false
if (!Array.isArray(values)) { if (!Array.isArray(values)) {
values = [values] values = [values]
no_list = true no_list = true
@@ -77,11 +77,11 @@ function convert_to_dnf(rules) {
rules = {} rules = {}
if (!no_list) { if (!no_list) {
rules[operator] = [] rules[operator] = []
for (var v of values) { for (let v of values) {
rules[operator].push(convert_to_dnf(v)) rules[operator].push(convertToDNF(v))
} }
} else { } else {
rules[operator] = convert_to_dnf(values[0]) rules[operator] = convertToDNF(values[0])
} }
if (JSON.stringify(old_rules) === JSON.stringify(rules)) { // Let's hope this is good enough... if (JSON.stringify(old_rules) === JSON.stringify(rules)) { // Let's hope this is good enough...
break break

View File

@@ -1,97 +1,116 @@
<template> <script setup lang="ts">
<select> import { ref, watch, onMounted, onUnmounted } from 'vue'
<slot></slot>
</select> declare const $: any
</template>
<script> export interface ObjectListItem {
export default { lookup: [string, number | string, string]
props: ["required", "value", "placeholder", "url", "multiple"],
template: ('<select>\n' +
' <slot></slot>\n' +
' </select>'),
mounted: function () {
this.build();
},
methods: {
build: function () {
var vm = this;
var multiple = this.multiple;
$(this.$el)
.empty()
.select2(this.opts())
.val(this.value || "")
.trigger("change")
// emit event on change.
.on("change", function (e) {
vm.$emit("input", $(this).select2('data'));
});
if (vm.value) {
for (var i = 0; i < vm.value["objectList"].length; i++) {
var option = new Option(vm.value["objectList"][i]["lookup"][2], vm.value["objectList"][i]["lookup"][1], true, true);
$(vm.$el).append(option);
} }
export interface ObjectList {
objectList: ObjectListItem[]
} }
$(vm.$el).trigger("change");
}, const props = defineProps<{
opts: function () { required?: boolean
value?: ObjectList
placeholder?: string
url?: string
multiple?: boolean
}>()
const emit = defineEmits<{
input: [value: any[]]
}>()
const select = ref<HTMLSelectElement | null>(null)
function opts () {
return { return {
theme: "bootstrap", theme: 'bootstrap',
delay: 100, delay: 100,
width: '100%', width: '100%',
multiple: true, multiple: true,
allowClear: this.required, allowClear: props.required,
language: $("body").attr("data-select2-locale"), language: $('body').attr('data-select2-locale'),
ajax: { ajax: {
url: this.url, url: props.url,
data: function (params) { data: function (params: { term: string; page?: number }) {
return { return {
query: params.term, query: params.term,
page: params.page || 1 page: params.page || 1
} }
} }
}, },
templateResult: function (res) { templateResult: function (res: { id?: string; text: string }) {
if (!res.id) { if (!res.id) {
return res.text; return res.text
} }
var $ret = $("<span>").append( const $ret = $('<span>').append(
$("<span>").addClass("primary").append($("<div>").text(res.text).html()) $('<span>').addClass('primary').append($('<div>').text(res.text).html())
); )
return $ret; return $ret
}, },
};
} }
}, }
watch: {
placeholder: function (val) { function build () {
$(this.$el).select2("destroy"); $(select.value)
this.build(); .empty()
}, .select2(opts())
required: function (val) { .val(props.value || '')
$(this.$el).select2("destroy"); .trigger('change')
this.build(); .on('change', function (this: HTMLElement) {
}, emit('input', $(this).select2('data'))
url: function (val) { })
$(this.$el).select2("destroy"); if (props.value) {
this.build(); for (let i = 0; i < props.value.objectList.length; i++) {
}, const option = new Option(props.value.objectList[i].lookup[2], String(props.value.objectList[i].lookup[1]), true, true)
value: function (newval, oldval) { $(select.value).append(option)
}
}
$(select.value).trigger('change')
}
watch(() => props.placeholder, () => {
$(select.value).select2('destroy')
build()
})
watch(() => props.required, () => {
$(select.value).select2('destroy')
build()
})
watch(() => props.url, () => {
$(select.value).select2('destroy')
build()
})
watch(() => props.value, (newval, oldval) => {
if (JSON.stringify(newval) !== JSON.stringify(oldval)) { if (JSON.stringify(newval) !== JSON.stringify(oldval)) {
$(this.$el).empty(); $(select.value).empty()
if (newval) { if (newval) {
for (var i = 0; i < newval["objectList"].length; i++) { for (let i = 0; i < newval.objectList.length; i++) {
var option = new Option(newval["objectList"][i]["lookup"][2], newval["objectList"][i]["lookup"][1], true, true); const option = new Option(newval.objectList[i].lookup[2], String(newval.objectList[i].lookup[1]), true, true)
$(this.$el).append(option); $(select.value).append(option)
} }
} }
$(this.$el).trigger("change"); $(select.value).trigger('change')
} }
}, })
},
destroyed: function () { onMounted(() => {
$(this.$el) build()
})
onUnmounted(() => {
$(select.value)
.off() .off()
.select2("destroy"); .select2('destroy')
} })
}
</script> </script>
<template lang="pug">
select(ref="select")
slot
</template>

View File

@@ -1,55 +1,45 @@
<template> <script setup lang="ts">
<input class="form-control"> import { ref, watch, onMounted, onUnmounted } from 'vue'
</template> import { DATETIME_OPTIONS } from './constants'
<script>
export default { const props = defineProps<{
props: ["required", "value"], required?: boolean
template: (''), value?: string
mounted: function () { }>()
var vm = this;
var multiple = this.multiple; const emit = defineEmits<{
$(this.$el) input: [value: string]
.datetimepicker(this.opts()) }>()
.trigger("change")
.on("dp.change", function (e) { const input = ref<HTMLInputElement | null>(null)
vm.$emit("input", $(this).data('DateTimePicker').date().format("HH:mm:ss"));
}); watch(() => props.value, (val) => {
if (!vm.value) { $(input.value).data('DateTimePicker').date(val)
$(this.$el).data("DateTimePicker").viewDate(moment().hour(0).minute(0).second(0).millisecond(0)); })
onMounted(() => {
$(input.value)
.datetimepicker({
...DATETIME_OPTIONS,
showClear: props.required,
})
.trigger('change')
.on('dp.change', function (this: HTMLElement) {
emit('input', $(this).data('DateTimePicker').date().format('HH:mm:ss'))
})
if (!props.value) {
$(input.value).data('DateTimePicker').viewDate(moment().hour(0).minute(0).second(0).millisecond(0))
} else { } else {
$(this.$el).data("DateTimePicker").date(vm.value); $(input.value).data('DateTimePicker').date(props.value)
} }
}, })
methods: {
opts: function () { onUnmounted(() => {
return { $(input.value)
format: $("body").attr("data-timeformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: this.required,
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove'
}
};
}
},
watch: {
value: function (val) {
$(this.$el).data('DateTimePicker').date(val);
},
},
destroyed: function () {
$(this.$el)
.off() .off()
.datetimepicker("destroy"); .datetimepicker('destroy')
} })
}
</script> </script>
<template lang="pug">
input.form-control(ref="input")
</template>

View File

@@ -1,140 +1,43 @@
<template> <script setup lang="ts">
<g> import { computed } from 'vue'
<path v-for="e in edges" :d="e" class="edge"></path> import { TEXTS, VARS, TYPEOPS } from './constants'
<path v-if="rootEdge" :d="rootEdge" class="edge"></path>
<path v-if="!node.children.length" :d="checkEdge" class="edge"></path>
<rect :width="boxWidth" :height="boxHeight" :x="x" :y="y" :class="nodeClass" rx="5">
</rect>
<foreignObject :width="boxWidth - 10" :height="boxHeight - 10" :x="x + 5" :y="y + 5"> declare const $: any
<div xmlns="http://www.w3.org/1999/xhtml" class="text"> declare const moment: any
<span v-if="vardata && vardata.type === 'int'">
<span v-if="variable.startsWith('entries_')" class="fa fa-sign-in"></span>
{{ vardata.label }}
<br>
<span v-if="varresult !== null">
{{varresult}}
</span>
<strong>
{{ op.label }} {{ rightoperand }}
</strong>
</span>
<span v-else-if="vardata && vardata.type === 'int_by_datetime'">
<span v-if="variable.startsWith('entries_')" class="fa fa-sign-in"></span>
{{ vardata.label }}
<span v-if="node.rule[operator][0][variable][0].buildTime[0] === 'custom'">
{{ df(node.rule[operator][0][variable][0].buildTime[1]) }}
</span>
<span v-else-if="node.rule[operator][0][variable][0].buildTime[0] === 'customtime'">
{{ tf(node.rule[operator][0][variable][0].buildTime[1]) }}
</span>
<span v-else>
{{ this.$root.texts[node.rule[operator][0][variable][0].buildTime[0]] }}
</span>
<br>
<span v-if="varresult !== null">
{{varresult}}
</span>
<strong>
{{ op.label }} {{ rightoperand }}
</strong>
</span>
<span v-else-if="vardata && variable === 'now'">
<span class="fa fa-clock-o"></span> {{ vardata.label }}<br>
<span v-if="varresult !== null">
{{varresult}}
</span>
<strong>
{{ op.label }}<br>
<span v-if="rightoperand.buildTime[0] === 'custom'">
{{ df(rightoperand.buildTime[1]) }}
</span>
<span v-else-if="rightoperand.buildTime[0] === 'customtime'">
{{ tf(rightoperand.buildTime[1]) }}
</span>
<span v-else>
{{ this.$root.texts[rightoperand.buildTime[0]] }}
</span>
<span v-if="operands[2]">
<span v-if="operator === 'isBefore'">+</span>
<span v-else>-</span>
{{ operands[2] }}
{{ this.$root.texts.minutes }}
</span>
</strong>
</span>
<span v-else-if="vardata && operator === 'inList'">
<span class="fa fa-sign-in" v-if="variable === 'gate'"></span>
<span class="fa fa-ticket" v-else></span>
{{ vardata.label }}
<span v-if="varresult !== null">
({{varresult}})
</span>
<br>
<strong>
{{ rightoperand.objectList.map((o) => o.lookup[2]).join(", ") }}
</strong>
</span>
<span v-else-if="vardata && vardata.type === 'enum_entry_status'">
<span class="fa fa-check-circle-o"></span>
{{ vardata.label }}
<span v-if="varresult !== null">
({{varresult}})
</span>
<br>
<strong>
{{ op.label }} {{ rightoperand }}
</strong>
</span>
</div>
</foreignObject>
<g v-if="result === false" :transform="`translate(${x + boxWidth - 15}, ${y - 10})`"> interface GraphNode {
<ellipse fill="#fff" cx="14.685823" cy="14.318233" rx="12.140151" ry="11.55523" /> rule: any
<path d="M 15,0 C 23.28125,0 30,6.71875 30,15 30,23.28125 23.28125,30 15,30 6.71875,30 0,23.28125 0,15 0,6.71875 6.71875,0 15,0 Z m 2.5,24.35547 V 20.64453 C 17.5,20.29297 17.22656,20 16.89453,20 h -3.75 C 12.79297,20 12.5,20.29297 12.5,20.64453 v 3.71094 C 12.5,24.70703 12.79297,25 13.14453,25 h 3.75 C 17.22656,25 17.5,24.70703 17.5,24.35547 Z M 17.4609,17.63672 17.81246,5.50781 c 0,-0.13672 -0.0586,-0.27343 -0.19531,-0.35156 C 17.49996,5.05855 17.32418,5 17.1484,5 h -4.29688 c -0.17578,0 -0.35156,0.0586 -0.46875,0.15625 -0.13672,0.0781 -0.19531,0.21484 -0.19531,0.35156 l 0.33203,12.12891 c 0,0.27344 0.29297,0.48828 0.66406,0.48828 h 3.61329 c 0.35156,0 0.64453,-0.21484 0.66406,-0.48828 z" column: number
class="error" /> children: string[]
</g> y: number
<g v-if="result === true" :transform="`translate(${x + boxWidth - 15}, ${y - 10})`"> parent?: GraphNode
<ellipse fill="#fff" cx="14.685823" cy="14.318233" rx="12.140151" ry="11.55523" /> }
<path d="m 25.078125,11.835938 c 0,-0.332032 -0.117188,-0.664063 -0.351563,-0.898438 L 22.949219,9.1796875 c -0.234375,-0.234375 -0.546875,-0.3710937 -0.878907,-0.3710937 -0.332031,0 -0.644531,0.1367187 -0.878906,0.3710937 L 13.222656,17.128906 8.8085938,12.714844 C 8.5742188,12.480469 8.2617188,12.34375 7.9296875,12.34375 c -0.3320313,0 -0.6445313,0.136719 -0.8789063,0.371094 l -1.7773437,1.757812 c -0.234375,0.234375 -0.3515625,0.566407 -0.3515625,0.898438 0,0.332031 0.1171875,0.644531 0.3515625,0.878906 l 7.0703125,7.070312 c 0.234375,0.234375 0.566406,0.371094 0.878906,0.371094 0.332032,0 0.664063,-0.136719 0.898438,-0.371094 L 24.726562,12.714844 c 0.234375,-0.234375 0.351563,-0.546875 0.351563,-0.878906 z M 30,15 C 30,23.28125 23.28125,30 15,30 6.71875,30 0,23.28125 0,15 0,6.71875 6.71875,0 15,0 23.28125,0 30,6.71875 30,15 Z"
class="check"/>
</g>
<g v-if="!node.children.length && (resultInclParents === null || resultInclParents === true)" :transform="`translate(${x + boxWidth + 25}, ${y + boxHeight/2 - 15})`">
<path d="m 25.078125,11.835938 c 0,-0.332032 -0.117188,-0.664063 -0.351563,-0.898438 L 22.949219,9.1796875 c -0.234375,-0.234375 -0.546875,-0.3710937 -0.878907,-0.3710937 -0.332031,0 -0.644531,0.1367187 -0.878906,0.3710937 L 13.222656,17.128906 8.8085938,12.714844 C 8.5742188,12.480469 8.2617188,12.34375 7.9296875,12.34375 c -0.3320313,0 -0.6445313,0.136719 -0.8789063,0.371094 l -1.7773437,1.757812 c -0.234375,0.234375 -0.3515625,0.566407 -0.3515625,0.898438 0,0.332031 0.1171875,0.644531 0.3515625,0.878906 l 7.0703125,7.070312 c 0.234375,0.234375 0.566406,0.371094 0.878906,0.371094 0.332032,0 0.664063,-0.136719 0.898438,-0.371094 L 24.726562,12.714844 c 0.234375,-0.234375 0.351563,-0.546875 0.351563,-0.878906 z M 30,15 C 30,23.28125 23.28125,30 15,30 6.71875,30 0,23.28125 0,15 0,6.71875 6.71875,0 15,0 23.28125,0 30,6.71875 30,15 Z"
class="check"/>
</g>
<g v-if="!node.children.length && (resultInclParents === false)" :transform="`translate(${x + boxWidth + 25}, ${y + boxHeight/2 - 15})`">
<path d="M 15,0 C 23.28125,0 30,6.71875 30,15 30,23.28125 23.28125,30 15,30 6.71875,30 0,23.28125 0,15 0,6.71875 6.71875,0 15,0 Z m 2.5,24.35547 V 20.64453 C 17.5,20.29297 17.22656,20 16.89453,20 h -3.75 C 12.79297,20 12.5,20.29297 12.5,20.64453 v 3.71094 C 12.5,24.70703 12.79297,25 13.14453,25 h 3.75 C 17.22656,25 17.5,24.70703 17.5,24.35547 Z M 17.4609,17.63672 17.81246,5.50781 c 0,-0.13672 -0.0586,-0.27343 -0.19531,-0.35156 C 17.49996,5.05855 17.32418,5 17.1484,5 h -4.29688 c -0.17578,0 -0.35156,0.0586 -0.46875,0.15625 -0.13672,0.0781 -0.19531,0.21484 -0.19531,0.35156 l 0.33203,12.12891 c 0,0.27344 0.29297,0.48828 0.66406,0.48828 h 3.61329 c 0.35156,0 0.64453,-0.21484 0.66406,-0.48828 z"
class="error" />
</g>
</g>
</template>
<script>
export default {
props: { const props = defineProps<{
node: Object, node: GraphNode
nodeid: String, nodeid: string
children: Array, children: GraphNode[]
boxWidth: Number, boxWidth: number
boxHeight: Number, boxHeight: number
marginX: Number, marginX: number
marginY: Number, marginY: number
paddingX: Number, paddingX: number
}, }>()
computed: {
x() { const x = computed(() => {
return this.node.column * (this.boxWidth + this.marginX) + this.marginX / 2 + this.paddingX return props.node.column * (props.boxWidth + props.marginX) + props.marginX / 2 + props.paddingX
}, })
y() {
return this.node.y * (this.boxHeight + this.marginY) + this.marginY / 2 const y = computed(() => {
}, return props.node.y * (props.boxHeight + props.marginY) + props.marginY / 2
edges() { })
const startX = this.x + this.boxWidth + 1
const startY = this.y + this.boxHeight / 2 const edges = computed(() => {
return this.children.map((c) => { const startX = x.value + props.boxWidth + 1
const endX = (c.column * (this.boxWidth + this.marginX) + this.marginX / 2 + this.paddingX) - 1 const startY = y.value + props.boxHeight / 2
const endY = (c.y * (this.boxHeight + this.marginY) + this.marginY / 2) + this.boxHeight / 2 return props.children.map((c) => {
const endX = (c.column * (props.boxWidth + props.marginX) + props.marginX / 2 + props.paddingX) - 1
const endY = (c.y * (props.boxHeight + props.marginY) + props.marginY / 2) + props.boxHeight / 2
return ` return `
M ${startX} ${startY} M ${startX} ${startY}
@@ -144,21 +47,23 @@
C ${endX - 25} ${endY} ${endX - 25} ${endY} ${endX} ${endY} C ${endX - 25} ${endY} ${endX - 25} ${endY} ${endX} ${endY}
` `
}) })
}, })
checkEdge() {
const startX = this.x + this.boxWidth + 1 const checkEdge = computed(() => {
const startY = this.y + this.boxHeight / 2 const startX = x.value + props.boxWidth + 1
const startY = y.value + props.boxHeight / 2
return `M ${startX} ${startY} L ${startX + 25} ${startY}` return `M ${startX} ${startY} L ${startX + 25} ${startY}`
}, })
rootEdge() {
if (this.node.column > 0) { const rootEdge = computed(() => {
if (props.node.column > 0) {
return return
} }
const startX = 0 const startX = 0
const startY = this.boxHeight / 2 + this.marginY / 2 const startY = props.boxHeight / 2 + props.marginY / 2
const endX = this.x - 1 const endX = x.value - 1
const endY = this.y + this.boxHeight / 2 const endY = y.value + props.boxHeight / 2
return ` return `
M ${startX} ${startY} M ${startX} ${startY}
@@ -167,89 +72,171 @@
L ${endX - 25} ${endY - 25 * Math.sign(endY - startY)} L ${endX - 25} ${endY - 25 * Math.sign(endY - startY)}
C ${endX - 25} ${endY} ${endX - 25} ${endY} ${endX} ${endY} C ${endX - 25} ${endY} ${endX - 25} ${endY} ${endX} ${endY}
` `
}, })
variable () {
const op = this.operator;
if (this.node.rule[op] && this.node.rule[op][0]) {
if (this.node.rule[op][0]["entries_since"]) {
return "entries_since";
}
if (this.node.rule[op][0]["entries_before"]) {
return "entries_before";
}
if (this.node.rule[op][0]["entries_days_since"]) {
return "entries_days_since";
}
if (this.node.rule[op][0]["entries_days_before"]) {
return "entries_days_before";
}
return this.node.rule[op][0]["var"];
} else {
return "";
}
},
vardata () {
return this.$root.VARS[this.variable];
},
varresult () {
const op = this.operator;
if (this.node.rule[op] && this.node.rule[op][0]) {
if (typeof this.node.rule[op][0]["__result"] === "undefined")
return null;
return this.node.rule[op][0]["__result"];
} else {
return "";
}
},
rightoperand () {
const op = this.operator;
if (this.node.rule[op] && typeof this.node.rule[op][1] !== "undefined") {
return this.node.rule[op][1];
} else {
return null;
}
},
op: function () {
return this.$root.TYPEOPS[this.vardata.type][this.operator]
},
operands: function () {
return this.node.rule[this.operator]
},
operator: function () {
return Object.keys(this.node.rule).filter(function (k) { return !k.startsWith("__") })[0];
},
result: function () {
return typeof this.node.rule.__result == "undefined" ? null : !!this.node.rule.__result
},
resultInclParents: function () {
if (typeof this.node.rule.__result == "undefined")
return null
function _p(node) { const operator = computed(() => {
return Object.keys(props.node.rule).filter((k) => !k.startsWith('__'))[0]
})
const variable = computed(() => {
const op = operator.value
if (props.node.rule[op] && props.node.rule[op][0]) {
if (props.node.rule[op][0]['entries_since']) {
return 'entries_since'
}
if (props.node.rule[op][0]['entries_before']) {
return 'entries_before'
}
if (props.node.rule[op][0]['entries_days_since']) {
return 'entries_days_since'
}
if (props.node.rule[op][0]['entries_days_before']) {
return 'entries_days_before'
}
return props.node.rule[op][0]['var']
} else {
return ''
}
})
const vardata = computed(() => {
return VARS[variable.value as keyof typeof VARS]
})
const varresult = computed(() => {
const op = operator.value
if (props.node.rule[op] && props.node.rule[op][0]) {
if (typeof props.node.rule[op][0]['__result'] === 'undefined')
return null
return props.node.rule[op][0]['__result']
} else {
return ''
}
})
const rightoperand = computed(() => {
const op = operator.value
if (props.node.rule[op] && typeof props.node.rule[op][1] !== 'undefined') {
return props.node.rule[op][1]
} else {
return null
}
})
const op = computed(() => {
return TYPEOPS[vardata.value.type as keyof typeof TYPEOPS]?.[operator.value as any]
})
const operands = computed(() => {
return props.node.rule[operator.value]
})
const result = computed(() => {
return typeof props.node.rule.__result === 'undefined' ? null : !!props.node.rule.__result
})
const resultInclParents = computed(() => {
if (typeof props.node.rule.__result === 'undefined') return null
function _p (node: GraphNode): boolean {
if (node.parent) { if (node.parent) {
return node.rule.__result && _p(node.parent) return node.rule.__result && _p(node.parent)
} }
return node.rule.__result return node.rule.__result
} }
return _p(this.node) return _p(props.node)
}, })
nodeClass: function () {
const nodeClass = computed(() => {
return { return {
"node": true, node: true,
"node-true": this.result === true, 'node-true': result.value === true,
"node-false": this.result === false, 'node-false': result.value === false,
} }
} })
},
methods: { function df (val: string) {
df (val) { const format = $('body').attr('data-datetimeformat')
const format = $("body").attr("data-datetimeformat")
return moment(val).format(format) return moment(val).format(format)
},
tf (val) {
const format = $("body").attr("data-timeformat")
return moment(val, "HH:mm:ss").format(format)
} }
},
function tf (val: string) {
const format = $('body').attr('data-timeformat')
return moment(val, 'HH:mm:ss').format(format)
} }
</script> </script>
<template lang="pug">
g
path.edge(v-for="e in edges", :key="e", :d="e")
path.edge(v-if="rootEdge", :d="rootEdge")
path.edge(v-if="!node.children.length", :d="checkEdge")
rect(:width="boxWidth", :height="boxHeight", :x="x", :y="y", :class="nodeClass", rx="5")
foreignObject(:width="boxWidth - 10", :height="boxHeight - 10", :x="x + 5", :y="y + 5")
div.text(xmlns="http://www.w3.org/1999/xhtml")
span(v-if="vardata && vardata.type === 'int'")
span.fa.fa-sign-in(v-if="variable.startsWith('entries_')")
| {{ vardata.label }}
br
span(v-if="varresult !== null") {{ varresult }}
strong
| {{ op.label }} {{ rightoperand }}
span(v-else-if="vardata && vardata.type === 'int_by_datetime'")
span.fa.fa-sign-in(v-if="variable.startsWith('entries_')")
| {{ vardata.label }}
span(v-if="node.rule[operator][0][variable][0].buildTime[0] === 'custom'")
| {{ df(node.rule[operator][0][variable][0].buildTime[1]) }}
span(v-else-if="node.rule[operator][0][variable][0].buildTime[0] === 'customtime'")
| {{ tf(node.rule[operator][0][variable][0].buildTime[1]) }}
span(v-else)
| {{ TEXTS[node.rule[operator][0][variable][0].buildTime[0]] }}
br
span(v-if="varresult !== null") {{ varresult }}
strong
| {{ op.label }} {{ rightoperand }}
span(v-else-if="vardata && variable === 'now'")
span.fa.fa-clock-o
| {{ vardata.label }}
br
span(v-if="varresult !== null") {{ varresult }}
strong
| {{ op.label }}
br
span(v-if="rightoperand.buildTime[0] === 'custom'")
| {{ df(rightoperand.buildTime[1]) }}
span(v-else-if="rightoperand.buildTime[0] === 'customtime'")
| {{ tf(rightoperand.buildTime[1]) }}
span(v-else)
| {{ TEXTS[rightoperand.buildTime[0]] }}
span(v-if="operands[2]")
span(v-if="operator === 'isBefore'") +
span(v-else) -
| {{ operands[2] }}
| {{ TEXTS.minutes }}
span(v-else-if="vardata && operator === 'inList'")
span.fa.fa-sign-in(v-if="variable === 'gate'")
span.fa.fa-ticket(v-else)
| {{ vardata.label }}
span(v-if="varresult !== null") ({{ varresult }})
br
strong
| {{ rightoperand.objectList.map((o: any) => o.lookup[2]).join(", ") }}
span(v-else-if="vardata && vardata.type === 'enum_entry_status'")
span.fa.fa-check-circle-o
| {{ vardata.label }}
span(v-if="varresult !== null") ({{ varresult }})
br
strong
| {{ op.label }} {{ rightoperand }}
g(v-if="result === false", :transform="`translate(${x + boxWidth - 15}, ${y - 10})`")
ellipse(fill="#fff", cx="14.685823", cy="14.318233", rx="12.140151", ry="11.55523")
path.error(d="M 15,0 C 23.28125,0 30,6.71875 30,15 30,23.28125 23.28125,30 15,30 6.71875,30 0,23.28125 0,15 0,6.71875 6.71875,0 15,0 Z m 2.5,24.35547 V 20.64453 C 17.5,20.29297 17.22656,20 16.89453,20 h -3.75 C 12.79297,20 12.5,20.29297 12.5,20.64453 v 3.71094 C 12.5,24.70703 12.79297,25 13.14453,25 h 3.75 C 17.22656,25 17.5,24.70703 17.5,24.35547 Z M 17.4609,17.63672 17.81246,5.50781 c 0,-0.13672 -0.0586,-0.27343 -0.19531,-0.35156 C 17.49996,5.05855 17.32418,5 17.1484,5 h -4.29688 c -0.17578,0 -0.35156,0.0586 -0.46875,0.15625 -0.13672,0.0781 -0.19531,0.21484 -0.19531,0.35156 l 0.33203,12.12891 c 0,0.27344 0.29297,0.48828 0.66406,0.48828 h 3.61329 c 0.35156,0 0.64453,-0.21484 0.66406,-0.48828 z")
g(v-if="result === true", :transform="`translate(${x + boxWidth - 15}, ${y - 10})`")
ellipse(fill="#fff", cx="14.685823", cy="14.318233", rx="12.140151", ry="11.55523")
path.check(d="m 25.078125,11.835938 c 0,-0.332032 -0.117188,-0.664063 -0.351563,-0.898438 L 22.949219,9.1796875 c -0.234375,-0.234375 -0.546875,-0.3710937 -0.878907,-0.3710937 -0.332031,0 -0.644531,0.1367187 -0.878906,0.3710937 L 13.222656,17.128906 8.8085938,12.714844 C 8.5742188,12.480469 8.2617188,12.34375 7.9296875,12.34375 c -0.3320313,0 -0.6445313,0.136719 -0.8789063,0.371094 l -1.7773437,1.757812 c -0.234375,0.234375 -0.3515625,0.566407 -0.3515625,0.898438 0,0.332031 0.1171875,0.644531 0.3515625,0.878906 l 7.0703125,7.070312 c 0.234375,0.234375 0.566406,0.371094 0.878906,0.371094 0.332032,0 0.664063,-0.136719 0.898438,-0.371094 L 24.726562,12.714844 c 0.234375,-0.234375 0.351563,-0.546875 0.351563,-0.878906 z M 30,15 C 30,23.28125 23.28125,30 15,30 6.71875,30 0,23.28125 0,15 0,6.71875 6.71875,0 15,0 23.28125,0 30,6.71875 30,15 Z")
g(v-if="!node.children.length && (resultInclParents === null || resultInclParents === true)", :transform="`translate(${x + boxWidth + 25}, ${y + boxHeight/2 - 15})`")
path.check(d="m 25.078125,11.835938 c 0,-0.332032 -0.117188,-0.664063 -0.351563,-0.898438 L 22.949219,9.1796875 c -0.234375,-0.234375 -0.546875,-0.3710937 -0.878907,-0.3710937 -0.332031,0 -0.644531,0.1367187 -0.878906,0.3710937 L 13.222656,17.128906 8.8085938,12.714844 C 8.5742188,12.480469 8.2617188,12.34375 7.9296875,12.34375 c -0.3320313,0 -0.6445313,0.136719 -0.8789063,0.371094 l -1.7773437,1.757812 c -0.234375,0.234375 -0.3515625,0.566407 -0.3515625,0.898438 0,0.332031 0.1171875,0.644531 0.3515625,0.878906 l 7.0703125,7.070312 c 0.234375,0.234375 0.566406,0.371094 0.878906,0.371094 0.332032,0 0.664063,-0.136719 0.898438,-0.371094 L 24.726562,12.714844 c 0.234375,-0.234375 0.351563,-0.546875 0.351563,-0.878906 z M 30,15 C 30,23.28125 23.28125,30 15,30 6.71875,30 0,23.28125 0,15 0,6.71875 6.71875,0 15,0 23.28125,0 30,6.71875 30,15 Z")
g(v-if="!node.children.length && (resultInclParents === false)", :transform="`translate(${x + boxWidth + 25}, ${y + boxHeight/2 - 15})`")
path.error(d="M 15,0 C 23.28125,0 30,6.71875 30,15 30,23.28125 23.28125,30 15,30 6.71875,30 0,23.28125 0,15 0,6.71875 6.71875,0 15,0 Z m 2.5,24.35547 V 20.64453 C 17.5,20.29297 17.22656,20 16.89453,20 h -3.75 C 12.79297,20 12.5,20.29297 12.5,20.64453 v 3.71094 C 12.5,24.70703 12.79297,25 13.14453,25 h 3.75 C 17.22656,25 17.5,24.70703 17.5,24.35547 Z M 17.4609,17.63672 17.81246,5.50781 c 0,-0.13672 -0.0586,-0.27343 -0.19531,-0.35156 C 17.49996,5.05855 17.32418,5 17.1484,5 h -4.29688 c -0.17578,0 -0.35156,0.0586 -0.46875,0.15625 -0.13672,0.0781 -0.19531,0.21484 -0.19531,0.35156 l 0.33203,12.12891 c 0,0.27344 0.29297,0.48828 0.66406,0.48828 h 3.61329 c 0.35156,0 0.64453,-0.21484 0.66406,-0.48828 z")
</template>