[#341] Added return block to ProcedureEditor.

This commit is contained in:
Fabio Manganiello 2024-09-12 02:02:27 +02:00
parent 152c2020de
commit b337bf7a53
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
5 changed files with 362 additions and 46 deletions

View file

@ -9,13 +9,12 @@
<div class="actions" :class="{dragging: isDragging}">
<div class="row item action"
v-for="(action, index) in newValue"
v-for="(action, index) in visibleActions"
:key="index">
<ConditionBlock v-bind="componentsData[index].props"
v-on="componentsData[index].on"
:collapsed="collapsedBlocks[index]"
:dragging="isDragging"
:has-else="!!elses[index + 1]"
@add-else="addElse"
v-if="conditions[index]" />
@ -26,12 +25,18 @@
:is-else="true"
v-else-if="elses[index]" />
<ReturnTile v-bind="componentsData[index].props"
:value="returnValue"
@change="editReturn($event)"
@delete="deleteAction(index)"
v-else-if="isReturn(action)" />
<ActionsListItem v-bind="componentsData[index].props"
v-on="componentsData[index].on"
v-else-if="isAction(action) && !collapsed" />
</div>
<div class="row item action add-action-container" v-if="!readOnly && !collapsed">
<div class="row item action add-action-container" v-if="visibleAddButtons.action">
<ListItem :active="isDragging"
:readOnly="false"
:spacerBottom="false"
@ -44,13 +49,24 @@
</ListItem>
</div>
<div class="row item action add-if-container" v-if="!readOnly && !collapsed">
<AddTile icon="fas fa-question" title="Add Condition" @click="addCondition" />
<div class="add-buttons-expander" v-if="showAddButtonsExpander">
<button @click.stop.prevent="collapseAddButtons = !collapseAddButtons">
<i class="fas" :class="collapseAddButtons ? 'fa-angle-down' : 'fa-angle-up'" />
</button>
</div>
<div class="row item action add-else-container"
v-if="!readOnly && !collapsed && parent && getCondition(parent) && !hasElse">
<AddTile icon="fas fa-question" title="Add Else" @click="$emit('add-else')" />
<div class="extra-add-buttons fade-in" v-if="showAddButtons">
<div class="row item action add-return-container" v-if="visibleAddButtons.return">
<AddTile icon="fas fa-angle-right" title="Add Return" @click="addReturn" />
</div>
<div class="row item action add-if-container" v-if="visibleAddButtons.condition">
<AddTile icon="fas fa-question" title="Add Condition" @click="addCondition" />
</div>
<div class="row item action add-else-container" v-if="visibleAddButtons.else">
<AddTile icon="fas fa-question" title="Add Else" @click="$emit('add-else')" />
</div>
</div>
</div>
</div>
@ -63,6 +79,7 @@ import AddTile from "./AddTile"
import ConditionBlock from "./ConditionBlock"
import ListItem from "./ListItem"
import Mixin from "./Mixin"
import ReturnTile from "./ReturnTile"
import Utils from "@/Utils"
export default {
@ -88,6 +105,7 @@ export default {
AddTile,
ConditionBlock,
ListItem,
ReturnTile,
},
props: {
@ -132,6 +150,7 @@ export default {
data() {
return {
collapseAddButtons: true,
newValue: [],
dragIndices: undefined,
initialValue: undefined,
@ -192,6 +211,14 @@ export default {
}
}
if (
this.getCondition(action) &&
this.newValue[index + 1] &&
this.isElse(this.newValue[index + 1])
) {
data.props.hasElse = true
}
if (this.isActionsBlock(action)) {
data.props.indent = this.indent + 1
}
@ -274,6 +301,91 @@ export default {
return this.dragIndices?.[0]
},
returnIndex() {
const ret = this.newValue?.reduce?.((acc, action, index) => {
if (acc >= 0)
return acc
if (this.isReturn(action))
return index
return acc
}, -1)
return ret >= 0 ? ret : null
},
returnValue() {
if (this.returnIndex == null)
return ''
const ret = this.newValue[this.returnIndex]
if (ret == null)
return ''
let retValue = null
if (Array.isArray(ret))
retValue = ret.length === 1 ? ret[0].match(/^return\s*(.*)$/)?.[1] : ret
else
retValue = ret.return
return retValue || ''
},
showAddButtons() {
return (
this.newValue.length === 0 || !this.collapseAddButtons
)
},
showAddButtonsExpander() {
return (
!this.readOnly &&
this.newValue?.length > 0 &&
Object.entries(this.visibleAddButtons).filter(
([key, value]) => value && key != 'action'
).length > 1
)
},
stopIndex() {
return this.returnIndex
},
visibleActions() {
return this.newValue.reduce((acc, action, index) => {
if (this.stopIndex != null && index > this.stopIndex)
return acc
if (
this.conditions[index] ||
this.elses[index] ||
this.isAction(action) ||
this.isReturn(action)
) {
acc[index] = action
}
return acc
}, {})
},
visibleAddButtons() {
return {
action: !this.readOnly && !this.collapsed && this.stopIndex == null,
return: !this.readOnly && !this.collapsed && this.stopIndex == null,
condition: !this.readOnly && !this.collapsed && this.stopIndex == null,
else: (
!this.readOnly &&
!this.collapsed &&
this.parent &&
this.getCondition(this.parent) &&
!this.hasElse &&
this.stopIndex == null
),
}
},
visibleTopSpacers() {
const dragIndex = this.dragIndex
return this.newValue.reduce((acc, tile, index) => {
@ -361,25 +473,22 @@ export default {
}
event.stopPropagation()
let dropIndices = []
if (!event.detail?.length) {
event = new CustomEvent(
'drop', {
bubbles: false,
cancelable: true,
detail: [dropIndex],
}
)
dropIndices = [dropIndex]
} else {
event = new CustomEvent(
'drop', {
bubbles: false,
cancelable: true,
detail: [dropIndex, ...event.detail],
}
)
dropIndices = [dropIndex, ...event.detail]
}
event = new CustomEvent(
'drop', {
bubbles: false,
cancelable: true,
detail: dropIndices,
}
)
if (this.indent > 0) {
// If the current drop location is within a nested block, then we need to
// bubble up the drop event to the parent block, until we reach the top block.
@ -389,7 +498,6 @@ export default {
// If we are at the root level, then we have the full picture of the underlying
// data structure, and we can perform the drop operation directly.
const dropIndices = event.detail
const dragIndex = this.dragIndices.slice(-1)[0]
dropIndex = event.detail.slice(-1)[0]
@ -435,12 +543,12 @@ export default {
let parent = this.newValue
while (parent && indices.length > 1) {
parent = parent[indices.shift()]
}
if (parent) {
const blockKey = this.getKey(parent)
if (blockKey) {
parent = parent[blockKey]
if (parent) {
const blockKey = this.getKey(parent)
if (blockKey) {
parent = parent[blockKey]
}
}
}
@ -462,11 +570,29 @@ export default {
},
addAction(action) {
this.newValue.push(action)
this.newValue.push(
{
...action,
action: action.name || action.action,
}
)
},
addCondition() {
this.newValue.push({ 'if ${True}': [] })
this.selectLastExprEditor()
},
addReturn() {
this.newValue.push({ 'return': null })
this.selectLastExprEditor()
},
editReturn(value) {
this.newValue[this.returnIndex] = { 'return': value?.length ? value : null }
},
selectLastExprEditor() {
this.$nextTick(() => {
const newTile = this.$refs[`action-tile-${this.newValue.length - 1}`]?.[0]
if (!newTile) {
@ -480,12 +606,12 @@ export default {
newTileElement.click()
this.$nextTick(() => {
const conditionEditor = newTile.$el?.querySelector('.condition-editor-container')
if (!conditionEditor) {
const exprEditor = newTile.$el?.querySelector('.expr-editor-container')
if (!exprEditor) {
return
}
const input = conditionEditor.querySelector('input[type="text"]')
const input = exprEditor.querySelector('input[type="text"]')
if (!input) {
return
}
@ -611,5 +737,28 @@ export default {
}
}
}
.add-buttons-expander {
width: 100%;
height: 1em;
margin: -0.5em 0 0.5em 0;
button {
width: 100%;
height: 100%;
background: none;
display: flex;
align-items: center;
justify-content: center;
border: none;
margin: 0;
padding: 0.5em 0 0 0;
&:hover {
color: $default-hover-fg;
}
}
display: flex;
}
}
</style>

View file

@ -46,17 +46,19 @@
<Modal title="Edit Condition"
:visible="true"
@close="showConditionEditor = false">
<ConditionEditor :value="value"
ref="conditionEditor"
@input.prevent.stop="onConditionChange"
v-if="showConditionEditor" />
<ExpressionEditor :value="value"
ref="conditionEditor"
@input.prevent.stop="onConditionChange"
v-if="showConditionEditor">
Condition
</ExpressionEditor>
</Modal>
</div>
</ListItem>
</template>
<script>
import ConditionEditor from "./ConditionEditor"
import ExpressionEditor from "./ExpressionEditor"
import ListItem from "./ListItem"
import Modal from "@/components/Modal"
import Tile from "@/components/elements/Tile"
@ -76,7 +78,7 @@ export default {
],
components: {
ConditionEditor,
ExpressionEditor,
ListItem,
Modal,
Tile,

View file

@ -1,11 +1,13 @@
<template>
<form class="condition-editor" @submit.prevent.stop="onSubmit">
<label for="condition">
Condition
<form class="expression-editor" @submit.prevent.stop="onSubmit">
<label for="expression">
<slot />
<input type="text"
name="condition"
name="expression"
autocomplete="off"
:autofocus="true"
:placeholder="placeholder"
:value="value"
ref="text"
@input.stop="onInput" />
@ -28,7 +30,17 @@ export default {
props: {
value: {
type: String,
required: true,
default: '',
},
allowEmpty: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: '',
},
},
@ -41,7 +53,7 @@ export default {
methods: {
onSubmit(event) {
const value = this.$refs.text.value.trim()
if (!value.length) {
if (!value.length && !this.allowEmpty) {
return
}
@ -52,7 +64,7 @@ export default {
onInput(event) {
const value = '' + event.target.value
if (!value?.trim()?.length) {
this.hasChanges = false
this.hasChanges = this.allowEmpty
} else {
this.hasChanges = value !== this.value
}
@ -70,6 +82,11 @@ export default {
},
mounted() {
this.hasChanges = false
if (!this.value?.trim()?.length) {
this.hasChanges = this.allowEmpty
}
this.$nextTick(() => {
this.$refs.text.focus()
})
@ -80,7 +97,7 @@ export default {
<style lang="scss" scoped>
@import "common";
.condition-editor {
.expression-editor {
min-width: 40em;
max-width: 100%;
display: flex;

View file

@ -31,6 +31,16 @@ export default {
isElse(value) {
return (this.getKey(value) || value)?.toLowerCase?.()?.trim?.() === 'else'
},
isReturn(value) {
if (!value)
return false
if (Array.isArray(value))
return value.length === 1 && value[0]?.length && value[0].match(/^return\s*$/i)
return this.getKey(value) === 'return'
},
},
}
</script>

View file

@ -0,0 +1,138 @@
<template>
<ListItem class="return-tile"
:value="value"
:active="active"
:read-only="readOnly"
:spacer-bottom="spacerBottom"
:spacer-top="spacerTop"
@input="$emit('input', $event)">
<Tile v-bind="tileConf.props"
v-on="tileConf.on"
@click.stop="showExprEditor = true">
<div class="tile-name">
<span class="icon">
<i class="fas fa-angle-right" />
</span>
<span class="name">
<span class="keyword">return</span> <span class="code" v-text="value" />
</span>
</div>
</Tile>
<div class="expr-editor-container" v-if="showExprEditor && !readOnly">
<Modal title="Edit Return"
:visible="true"
@close="showExprEditor = false">
<ExpressionEditor :value="value"
:allow-empty="true"
placeholder="Optional return value"
ref="exprEditor"
@input.prevent.stop="onExprChange"
v-if="showExprEditor">
Value or Expression
</ExpressionEditor>
</Modal>
</div>
</ListItem>
</template>
<script>
import ExpressionEditor from "./ExpressionEditor"
import ListItem from "./ListItem"
import Modal from "@/components/Modal"
import Tile from "@/components/elements/Tile"
export default {
emits: [
'change',
'click',
'delete',
'input',
],
components: {
ExpressionEditor,
ListItem,
Modal,
Tile,
},
props: {
value: {
type: String,
default: '',
},
active: {
type: Boolean,
default: false,
},
isElse: {
type: Boolean,
default: false,
},
readOnly: {
type: Boolean,
default: false,
},
spacerBottom: {
type: Boolean,
default: true,
},
spacerTop: {
type: Boolean,
default: true,
},
},
computed: {
tileConf() {
return {
props: {
value: this.value,
class: 'keyword',
draggable: false,
readOnly: this.readOnly,
withDelete: !this.readOnly,
},
on: {
delete: () => this.$emit('delete'),
input: this.onInput,
},
}
},
},
data() {
return {
showExprEditor: false,
}
},
methods: {
onExprChange(event) {
this.showExprEditor = false
if (this.readOnly) {
return
}
const expr = event.target.value?.trim()
event.target.value = expr
this.$emit('change', expr)
},
onInput(value) {
if (!value || this.readOnly) {
return
}
this.$emit('input', value)
},
},
}
</script>