forked from platypush/platypush
[Procedure Editor] Added support for conditions and nested blocks.
This commit is contained in:
parent
202cff093f
commit
471ec1370c
15 changed files with 1881 additions and 270 deletions
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="action-tile-container">
|
<div class="action-tile-container">
|
||||||
<div class="action-tile"
|
<div class="action-tile"
|
||||||
|
:class="{ new: isNew }"
|
||||||
ref="tile"
|
ref="tile"
|
||||||
@click="$refs.actionEditor.show">
|
@click="$refs.actionEditor.show">
|
||||||
<div class="action-delete"
|
<div class="action-delete"
|
||||||
|
@ -122,6 +123,10 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
isNew() {
|
||||||
|
return !this.readOnly && !this.name?.length
|
||||||
|
},
|
||||||
|
|
||||||
name() {
|
name() {
|
||||||
return this.value.name || this.value.action
|
return this.value.name || this.value.action
|
||||||
},
|
},
|
||||||
|
@ -179,6 +184,14 @@ export default {
|
||||||
background: $tile-hover-bg;
|
background: $tile-hover-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.new {
|
||||||
|
background: $tile-bg-3;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $tile-hover-bg-3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.action-delete {
|
.action-delete {
|
||||||
width: 1.5em;
|
width: 1.5em;
|
||||||
height: 1.5em;
|
height: 1.5em;
|
||||||
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
<template>
|
||||||
|
<div class="actions-block" :class="{ hover }">
|
||||||
|
<slot name="before" />
|
||||||
|
|
||||||
|
<div class="actions-list-container" ref="actionsListContainer">
|
||||||
|
<button class="collapse-button"
|
||||||
|
@click="collapsed_ = !collapsed_"
|
||||||
|
v-if="isCollapsed">
|
||||||
|
<i class="fas fa-ellipsis-h" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="actions-list" :class="actionListClasses">
|
||||||
|
<ActionsList :value="value[key]"
|
||||||
|
:dragging="dragging"
|
||||||
|
:has-else="hasElse"
|
||||||
|
:indent="indent"
|
||||||
|
:parent="value"
|
||||||
|
:read-only="readOnly"
|
||||||
|
@add-else="$emit('add-else')"
|
||||||
|
@collapse="collapsed_ = !collapsed_"
|
||||||
|
@drag="$emit('drag', $event)"
|
||||||
|
@dragend="$emit('dragend', $event); hover = false"
|
||||||
|
@dragenter="$emit('dragenter', $event)"
|
||||||
|
@dragleave="$emit('dragleave', $event); hover = false"
|
||||||
|
@dragover="$emit('dragover', $event)"
|
||||||
|
@drop="$emit('drop', $event); hover = false"
|
||||||
|
@input="$emit('input', $event); hover = false" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<slot name="after" />
|
||||||
|
|
||||||
|
<Droppable :element="$refs.actionsListContainer"
|
||||||
|
@dragenter="onDragEnter"
|
||||||
|
@dragleave="onDragLeave"
|
||||||
|
@drop="hover = false"
|
||||||
|
v-if="!readOnly" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineAsyncComponent } from "vue"
|
||||||
|
import Droppable from "@/components/elements/Droppable"
|
||||||
|
import Mixin from "./Mixin"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ActionsBlock',
|
||||||
|
mixins: [Mixin],
|
||||||
|
emits: [
|
||||||
|
'add-else',
|
||||||
|
'drag',
|
||||||
|
'dragend',
|
||||||
|
'dragenter',
|
||||||
|
'dragleave',
|
||||||
|
'dragover',
|
||||||
|
'drop',
|
||||||
|
'input',
|
||||||
|
],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
// Handle indirect circular dependency
|
||||||
|
ActionsList: defineAsyncComponent(() => import('./ActionsList')),
|
||||||
|
Droppable,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
collapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
dragging: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
indent: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
hasElse: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
actionListClasses() {
|
||||||
|
return {
|
||||||
|
hidden: this.isCollapsed,
|
||||||
|
fold: this.folding,
|
||||||
|
unfold: this.unfolding,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
condition() {
|
||||||
|
return this.getCondition(this.key)
|
||||||
|
},
|
||||||
|
|
||||||
|
isCollapsed() {
|
||||||
|
const transitioning = this.hover || this.folding || this.unfolding
|
||||||
|
if (transitioning) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.collapsed_) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.collapsed
|
||||||
|
},
|
||||||
|
|
||||||
|
key() {
|
||||||
|
return this.getKey(this.value)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
collapsed_: false,
|
||||||
|
folding: false,
|
||||||
|
hover: false,
|
||||||
|
hoverTimeout: null,
|
||||||
|
unfolding: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
collapsed_(value) {
|
||||||
|
if (value) {
|
||||||
|
this.folding = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.folding = false
|
||||||
|
}, 300)
|
||||||
|
} else {
|
||||||
|
this.unfolding = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.unfolding = false
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onDragEnter() {
|
||||||
|
if (this.hoverTimeout) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hoverTimeout = setTimeout(() => {
|
||||||
|
this.hover = true
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragLeave() {
|
||||||
|
if (this.hoverTimeout) {
|
||||||
|
clearTimeout(this.hoverTimeout)
|
||||||
|
this.hoverTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hover = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions-block {
|
||||||
|
.collapse-button {
|
||||||
|
width: 100%;
|
||||||
|
background: none !important;
|
||||||
|
margin-left: 1em;
|
||||||
|
font-size: 0.85em;
|
||||||
|
text-align: left;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hover {
|
||||||
|
.collapse-button {
|
||||||
|
color: $default-hover-fg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,28 +1,57 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="actions" :class="{dragging: dragItem != null}">
|
<div class="actions-list">
|
||||||
<div class="row item action" v-for="(action, index) in actions" :key="index">
|
<div class="indent-spacers" v-if="indent > 0">
|
||||||
<ActionsListItem :value="action"
|
<div class="indent-spacer" @click="onCollapse">
|
||||||
:active="dragItem != null"
|
<div class="left side" />
|
||||||
:read-only="readOnly"
|
<div class="right side" />
|
||||||
:spacer-bottom="visibleBottomSpacers[index]"
|
</div>
|
||||||
:spacer-top="visibleTopSpacers[index]"
|
|
||||||
:ref="`action-tile-${index}`"
|
|
||||||
@delete="deleteAction(index)"
|
|
||||||
@drag="dragItem = index"
|
|
||||||
@dragend.prevent="dragItem = null"
|
|
||||||
@dragenterspacer.prevent="dropIndex = index"
|
|
||||||
@dragleavespacer.prevent="dropIndex = undefined"
|
|
||||||
@dragover.prevent="onTileDragOver($event, index)"
|
|
||||||
@dragoverspacer.prevent="dropIndex = index"
|
|
||||||
@drop="onDrop(index)"
|
|
||||||
@input="editAction($event, index)" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row item action">
|
<div class="actions" :class="{dragging: isDragging}">
|
||||||
|
<div class="row item action"
|
||||||
|
v-for="(action, index) in newValue"
|
||||||
|
: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]" />
|
||||||
|
|
||||||
|
<ConditionBlock v-bind="componentsData[index].props"
|
||||||
|
v-on="componentsData[index].on"
|
||||||
|
:collapsed="collapsedBlocks[index]"
|
||||||
|
:dragging="isDragging"
|
||||||
|
:is-else="true"
|
||||||
|
v-else-if="elses[index]" />
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<ListItem :active="isDragging"
|
||||||
|
:readOnly="false"
|
||||||
|
:spacerBottom="false"
|
||||||
|
:spacerTop="!newValue.length"
|
||||||
|
:value="newAction"
|
||||||
|
@drop="onDrop(0, $event)">
|
||||||
<ActionTile :value="newAction"
|
<ActionTile :value="newAction"
|
||||||
:draggable="false"
|
:draggable="false"
|
||||||
@input="addAction"
|
@input="addAction" />
|
||||||
v-if="!readOnly" />
|
</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>
|
||||||
|
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -30,17 +59,68 @@
|
||||||
<script>
|
<script>
|
||||||
import ActionsListItem from "./ActionsListItem"
|
import ActionsListItem from "./ActionsListItem"
|
||||||
import ActionTile from "./ActionTile"
|
import ActionTile from "./ActionTile"
|
||||||
|
import AddTile from "./AddTile"
|
||||||
|
import ConditionBlock from "./ConditionBlock"
|
||||||
|
import ListItem from "./ListItem"
|
||||||
|
import Mixin from "./Mixin"
|
||||||
import Utils from "@/Utils"
|
import Utils from "@/Utils"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [Utils],
|
name: 'ActionsList',
|
||||||
emits: ['input'],
|
mixins: [Mixin, Utils],
|
||||||
|
emits: [
|
||||||
|
'add-else',
|
||||||
|
'change',
|
||||||
|
'collapse',
|
||||||
|
'drag',
|
||||||
|
'dragend',
|
||||||
|
'dragenter',
|
||||||
|
'dragleave',
|
||||||
|
'dragover',
|
||||||
|
'drop',
|
||||||
|
'input',
|
||||||
|
'reset',
|
||||||
|
],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
ActionsListItem,
|
ActionsListItem,
|
||||||
ActionTile,
|
ActionTile,
|
||||||
|
AddTile,
|
||||||
|
ConditionBlock,
|
||||||
|
ListItem,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
collapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
dragging: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
indent: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
hasElse: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
parent: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
value: {
|
value: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({
|
default: () => ({
|
||||||
|
@ -48,112 +128,399 @@ export default {
|
||||||
actions: [],
|
actions: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
readOnly: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
actions: [],
|
newValue: [],
|
||||||
dragItem: undefined,
|
dragIndices: undefined,
|
||||||
dropIndex: undefined,
|
initialValue: undefined,
|
||||||
newAction: {},
|
newAction: {},
|
||||||
spacerElements: {},
|
spacerElements: {},
|
||||||
visibleTopSpacers: {},
|
|
||||||
visibleBottomSpacers: {},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
collapsedBlocks() {
|
||||||
|
return this.newValue.reduce((acc, action, index) => {
|
||||||
|
if (!this.isActionsBlock(action)) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isDragging) {
|
||||||
|
acc[index] = this.collapsed
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.elses[index]) {
|
||||||
|
acc[index] = this.dragBlockIndex === index - 1
|
||||||
|
} else {
|
||||||
|
acc[index] = this.dragBlockIndex === index
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
componentsData() {
|
||||||
|
return this.newValue.map((action, index) => {
|
||||||
|
let data = {
|
||||||
|
props: {
|
||||||
|
value: action,
|
||||||
|
active: this.isDragging,
|
||||||
|
readOnly: this.readOnly,
|
||||||
|
ref: `action-tile-${index}`,
|
||||||
|
spacerBottom: this.visibleBottomSpacers[index],
|
||||||
|
spacerTop: this.visibleTopSpacers[index],
|
||||||
|
},
|
||||||
|
|
||||||
|
on: {
|
||||||
|
delete: () => this.deleteAction(index),
|
||||||
|
drag: (event) => this.onDragStart(index, event),
|
||||||
|
dragend: (event) => this.onDragEnd(event),
|
||||||
|
dragenter: (event) => this.onDragEnter(index, event),
|
||||||
|
dragleave: (event) => this.onDragLeave(index, event),
|
||||||
|
dragover: (event) => this.onDragOver(event),
|
||||||
|
drop: (event) => {
|
||||||
|
try {
|
||||||
|
this.onDrop(index, event)
|
||||||
|
} finally {
|
||||||
|
this.isDragging = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
input: (value) => this.editAction(value, index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isActionsBlock(action)) {
|
||||||
|
data.props.indent = this.indent + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
conditions() {
|
||||||
|
return this.newValue?.reduce?.((acc, action, index) => {
|
||||||
|
const condition = this.getCondition(action)
|
||||||
|
if (condition) {
|
||||||
|
acc[index] = {
|
||||||
|
condition,
|
||||||
|
actions: action[Object.keys(action)[0]],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {}) || {}
|
||||||
|
},
|
||||||
|
|
||||||
|
dragBlockIndex() {
|
||||||
|
if (this.dragIndex == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return an index only if the dragged item is the actual actions block
|
||||||
|
// and not one of its children.
|
||||||
|
if (!(this.dragIndices?.length === 1 && this.dragIndices[0] === this.dragIndex)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return an index only if the dragged item is an actions block.
|
||||||
|
if (!this.isActionsBlock(this.newValue[this.dragIndex])) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dragIndex
|
||||||
|
},
|
||||||
|
|
||||||
|
isDragging: {
|
||||||
|
get() {
|
||||||
|
return this.dragging || (this.dragIndices?.length || 0) > 0
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
if (!value) {
|
||||||
|
this.dragIndices = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
elses() {
|
||||||
|
return this.newValue?.reduce?.((acc, action, index) => {
|
||||||
|
if (this.isElse(action) && this.conditions[index - 1]) {
|
||||||
|
acc[index] = action[Object.keys(action)[0]]
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {}) || {}
|
||||||
|
},
|
||||||
|
|
||||||
hasChanges() {
|
hasChanges() {
|
||||||
return JSON.stringify(this.value) !== JSON.stringify(this.actions)
|
return this.newStringValue !== this.stringValue
|
||||||
|
},
|
||||||
|
|
||||||
|
stringValue() {
|
||||||
|
return JSON.stringify(this.value)
|
||||||
|
},
|
||||||
|
|
||||||
|
newStringValue() {
|
||||||
|
return JSON.stringify(this.newValue)
|
||||||
|
},
|
||||||
|
|
||||||
|
dragIndex() {
|
||||||
|
if (!this.isDragging) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dragIndices?.[0]
|
||||||
|
},
|
||||||
|
|
||||||
|
visibleTopSpacers() {
|
||||||
|
const dragIndex = this.dragIndex
|
||||||
|
return this.newValue.reduce((acc, tile, index) => {
|
||||||
|
acc[index] = (
|
||||||
|
!this.isElse(tile) && (
|
||||||
|
dragIndex == null ||
|
||||||
|
dragIndex > index || (
|
||||||
|
dragIndex === index &&
|
||||||
|
this.dragIndices.length > 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
visibleBottomSpacers() {
|
||||||
|
const dragIndex = this.dragIndex
|
||||||
|
return this.newValue.reduce((acc, _, index) => {
|
||||||
|
acc[index] = (
|
||||||
|
(
|
||||||
|
dragIndex != null && (
|
||||||
|
dragIndex < index || (
|
||||||
|
dragIndex === index &&
|
||||||
|
this.dragIndices.length > 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) || (
|
||||||
|
dragIndex == null &&
|
||||||
|
index === this.newValue.length - 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onDrop(index) {
|
onDragStart(index, event) {
|
||||||
if (this.dragItem == null || this.readOnly)
|
if (this.readOnly)
|
||||||
return
|
return
|
||||||
|
|
||||||
this.actions.splice(
|
if (Array.isArray(event)) {
|
||||||
index, 0, this.actions.splice(this.dragItem, 1)[0]
|
event = [index, ...event]
|
||||||
|
} else {
|
||||||
|
event = [index]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dragIndices = event
|
||||||
|
this.$emit('drag', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragEnd() {
|
||||||
|
this.isDragging = false
|
||||||
|
this.$emit('dragend')
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragEnter(index, event) {
|
||||||
|
if (!this.isDragging || this.readOnly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
this.$emit('dragenter', index)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragLeave(index, event) {
|
||||||
|
if (!this.isDragging || this.readOnly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
this.$emit('dragleave', index)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragOver(event) {
|
||||||
|
this.$emit('dragover', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDrop(dropIndex, event) {
|
||||||
|
if (!this.isDragging || event == null || dropIndex == null || this.readOnly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
if (!event.detail?.length) {
|
||||||
|
event = new CustomEvent(
|
||||||
|
'drop', {
|
||||||
|
bubbles: false,
|
||||||
|
cancelable: true,
|
||||||
|
detail: [dropIndex],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
event = new CustomEvent(
|
||||||
|
'drop', {
|
||||||
|
bubbles: false,
|
||||||
|
cancelable: true,
|
||||||
|
detail: [dropIndex, ...event.detail],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
this.$emit('drop', event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]
|
||||||
|
|
||||||
|
// Get the parent blocks of the dragged and dropped items.
|
||||||
|
const dragParent = this.getParentBlock(this.dragIndices)
|
||||||
|
const dropParent = this.getParentBlock(dropIndices)
|
||||||
|
if (!(dragParent && dropParent)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragItem = dragParent?.[dragIndex]
|
||||||
|
const dropItem = dropParent?.[dropIndex]
|
||||||
|
if (!dragItem) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the dragged item is a condition, then we need to update the else block as well.
|
||||||
|
const draggedItems = (
|
||||||
|
this.getCondition(dragItem) && this.isElse(dragParent[dragIndex + 1]) ? 2 : 1
|
||||||
)
|
)
|
||||||
|
|
||||||
this.dragItem = null
|
// If the drop location is an else block, then the target needs to be the next
|
||||||
this.dropIndex = null
|
// slot, or we'll break the if-else chain.
|
||||||
|
if (this.isElse(dropItem)) {
|
||||||
|
dropIndex += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dropParent.splice(
|
||||||
|
dropIndex, 0, ...dragParent.splice(dragIndex, draggedItems)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Emit the drop event to the parent block, so that it can update its
|
||||||
|
// view of the data structure.
|
||||||
|
this.$emit('input', this.newValue)
|
||||||
},
|
},
|
||||||
|
|
||||||
onTileDragOver(event, index) {
|
onCollapse() {
|
||||||
if (this.dragItem == null || this.readOnly || this.dragItem === index)
|
this.$emit('collapse')
|
||||||
return
|
|
||||||
|
|
||||||
const dragOverTile = this.$refs[`action-tile-${index}`]?.[0]
|
|
||||||
const dragOverTileEl = dragOverTile?.$el?.nextSibling
|
|
||||||
if (!dragOverTileEl)
|
|
||||||
return
|
|
||||||
|
|
||||||
const rootTop = this.$el.getBoundingClientRect().top
|
|
||||||
const cursorY = event.clientY - rootTop
|
|
||||||
const dragOverTilePos = {
|
|
||||||
top: dragOverTileEl.offsetTop,
|
|
||||||
bottom: dragOverTileEl.offsetTop + dragOverTileEl.offsetHeight,
|
|
||||||
}
|
|
||||||
|
|
||||||
const dragOverTileSectionHeight = (dragOverTilePos.bottom - dragOverTilePos.top) / 3
|
|
||||||
let cursorTileSection = null
|
|
||||||
|
|
||||||
if (cursorY < dragOverTilePos.top + dragOverTileSectionHeight) {
|
|
||||||
cursorTileSection = 'top'
|
|
||||||
} else if (cursorY < dragOverTilePos.bottom - dragOverTileSectionHeight) {
|
|
||||||
cursorTileSection = 'middle'
|
|
||||||
} else {
|
|
||||||
cursorTileSection = 'bottom'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursorTileSection === 'middle') {
|
|
||||||
this.dropIndex = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursorTileSection === 'top' && index < this.dragItem) {
|
|
||||||
this.dropIndex = index
|
|
||||||
} else if (cursorTileSection === 'bottom' && index > this.dragItem) {
|
|
||||||
if (index === this.actions.length - 1) {
|
|
||||||
this.dropIndex = index + 1
|
|
||||||
} else {
|
|
||||||
this.dropIndex = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
editAction(action, index) {
|
getParentBlock(indices) {
|
||||||
this.actions[index] = action
|
indices = [...indices]
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent
|
||||||
|
},
|
||||||
|
|
||||||
|
editAction(event, index) {
|
||||||
|
if (event?.target && event.stopPropagation) {
|
||||||
|
// If the event is a native event, then we need to stop the propagation,
|
||||||
|
// otherwise the event will be caught by the parent element. If the parent
|
||||||
|
// is a modal, then the modal will be closed, making it impossible to edit
|
||||||
|
// text fields in the action tiles.
|
||||||
|
event.stopPropagation()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.newValue[index] = event
|
||||||
|
this.$emit('input', this.newValue)
|
||||||
},
|
},
|
||||||
|
|
||||||
addAction(action) {
|
addAction(action) {
|
||||||
this.actions.push(action)
|
this.newValue.push(action)
|
||||||
|
},
|
||||||
|
|
||||||
|
addCondition() {
|
||||||
|
this.newValue.push({ 'if ${True}': [] })
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const newTile = this.$refs[`action-tile-${this.newValue.length - 1}`]?.[0]
|
||||||
|
if (!newTile) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTileElement = newTile.$el?.querySelector('.tile')
|
||||||
|
if (!newTileElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newTileElement.click()
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const conditionEditor = newTile.$el?.querySelector('.condition-editor-container')
|
||||||
|
if (!conditionEditor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = conditionEditor.querySelector('input[type="text"]')
|
||||||
|
if (!input) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
input.value = ''
|
||||||
|
input.focus()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
addElse() {
|
||||||
|
this.newValue.push({ 'else': [] })
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteAction(index) {
|
deleteAction(index) {
|
||||||
this.actions.splice(index, 1)
|
// If the action is a condition, then we need to also remove the else block
|
||||||
},
|
const items = (
|
||||||
|
this.getCondition(this.newValue[index]) && this.isElse(this.newValue?.[index + 1])
|
||||||
|
) ? 2 : 1
|
||||||
|
|
||||||
resetSpacers() {
|
const el = this.$refs[`action-tile-${index}`]?.[0]?.$el
|
||||||
this.visibleTopSpacers = Object.keys(this.actions).reduce((acc, index) => {
|
if (el) {
|
||||||
acc[index] = true
|
el.classList.add('shrink')
|
||||||
return acc
|
setTimeout(() => {
|
||||||
}, {})
|
el.classList.remove('shrink')
|
||||||
|
this.newValue.splice(index, items)
|
||||||
this.visibleBottomSpacers = {[this.actions.length - 1]: true}
|
}, 300)
|
||||||
this.syncSpacers()
|
} else {
|
||||||
|
this.newValue.splice(index, items)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
syncSpacers() {
|
syncSpacers() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.spacerElements = Object.keys(this.actions).reduce((acc, index) => {
|
this.spacerElements = Object.keys(this.newValue).reduce((acc, index) => {
|
||||||
acc[index] = this.$refs[`dropTarget_${index}`]?.[0]
|
acc[index] = this.$refs[`dropTarget_${index}`]?.[0]
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
@ -164,35 +531,21 @@ export default {
|
||||||
if (!this.value || !this.hasChanges)
|
if (!this.value || !this.hasChanges)
|
||||||
return
|
return
|
||||||
|
|
||||||
this.actions = this.value
|
this.newValue = this.value
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
actions: {
|
newValue: {
|
||||||
deep: true,
|
deep: true,
|
||||||
handler(value) {
|
handler(value) {
|
||||||
this.$emit('input', value)
|
this.$emit('input', value)
|
||||||
this.resetSpacers()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
dragItem(value) {
|
|
||||||
if (value == null || this.readOnly) {
|
|
||||||
this.resetSpacers()
|
|
||||||
} else {
|
|
||||||
this.visibleTopSpacers = Object.keys(this.actions).reduce((acc, index) => {
|
|
||||||
acc[index] = this.dragItem > index
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
this.visibleBottomSpacers = Object.keys(this.actions).reduce((acc, index) => {
|
|
||||||
acc[index] = this.dragItem < index
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
this.syncSpacers()
|
this.syncSpacers()
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
dragIndices() {
|
||||||
|
this.syncSpacers()
|
||||||
},
|
},
|
||||||
|
|
||||||
value: {
|
value: {
|
||||||
|
@ -206,17 +559,57 @@ export default {
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.syncValue()
|
this.syncValue()
|
||||||
this.resetSpacers()
|
this.syncSpacers()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
$spacer-height: 1em;
|
.actions-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
.action {
|
.action {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-action-container {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indent-spacers {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.indent-spacer {
|
||||||
|
width: 1.5em;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.side {
|
||||||
|
width: 0.75em;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
border-right: 1px solid $selected-fg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.side.left {
|
||||||
|
border-right: 1px solid $tile-code-fg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,68 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="row item action" :class="{ dragging }">
|
<ListItem class="action"
|
||||||
<div class="spacer-wrapper" :class="{ hidden: !spacerTop }">
|
:active="active"
|
||||||
<div class="spacer"
|
:dragging="dragging"
|
||||||
:class="{ active }"
|
:spacer-bottom="spacerBottom"
|
||||||
ref="dropTargetTop">
|
:spacer-top="spacerTop"
|
||||||
<div class="droppable-wrapper">
|
:value="value"
|
||||||
<div class="droppable-container">
|
v-on="componentsData.on">
|
||||||
<div class="droppable-frame">
|
|
||||||
<div class="droppable" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Droppable :element="$refs.dropTargetTop"
|
|
||||||
:disabled="readOnly"
|
|
||||||
@dragend.prevent="onDrop"
|
|
||||||
@dragenter.prevent="$emit('dragenterspacer', $event)"
|
|
||||||
@dragleave.prevent="$emit('dragleavespacer', $event)"
|
|
||||||
@dragover.prevent="$emit('dragoverspacer', $event)"
|
|
||||||
@drop="$emit('drop', $event)" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="spacer" v-if="dragging" />
|
|
||||||
|
|
||||||
<ActionTile :value="value"
|
<ActionTile :value="value"
|
||||||
:draggable="!readOnly"
|
:draggable="!readOnly"
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
:with-delete="!readOnly"
|
:with-delete="!readOnly"
|
||||||
|
v-on="componentsData.on"
|
||||||
@contextmenu="$emit('contextmenu', $event)"
|
@contextmenu="$emit('contextmenu', $event)"
|
||||||
@drag="onDragStart"
|
@drag.stop="onDragStart"
|
||||||
@dragend.prevent="onDragEnd"
|
|
||||||
@dragover.prevent="$emit('dragover', $event)"
|
|
||||||
@drop="onDrop"
|
|
||||||
@input="$emit('input', $event)"
|
|
||||||
@delete="$emit('delete', $event)" />
|
@delete="$emit('delete', $event)" />
|
||||||
|
</ListItem>
|
||||||
<div class="spacer" v-if="dragging" />
|
|
||||||
|
|
||||||
<div class="spacer-wrapper" :class="{ hidden: !spacerBottom }">
|
|
||||||
<div class="spacer" :class="{ active }" ref="dropTargetBottom">
|
|
||||||
<div class="droppable-wrapper">
|
|
||||||
<div class="droppable-container">
|
|
||||||
<div class="droppable-frame">
|
|
||||||
<div class="droppable" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Droppable :element="$refs.dropTargetBottom"
|
|
||||||
:disabled="readOnly"
|
|
||||||
@dragend.prevent="onDrop"
|
|
||||||
@dragenter.prevent="$emit('dragenterspacer', $event)"
|
|
||||||
@dragleave.prevent="$emit('dragleavespacer', $event)"
|
|
||||||
@dragover.prevent="$emit('dragoverspacer', $event)"
|
|
||||||
@drop="onDrop" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ActionTile from "@/components/Action/ActionTile"
|
import ActionTile from "@/components/Action/ActionTile"
|
||||||
import Droppable from "@/components/elements/Droppable"
|
import ListItem from "@/components/Action/ListItem"
|
||||||
import Utils from "@/Utils"
|
import Utils from "@/Utils"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -73,18 +30,15 @@ export default {
|
||||||
'drag',
|
'drag',
|
||||||
'dragend',
|
'dragend',
|
||||||
'dragenter',
|
'dragenter',
|
||||||
'dragenterspacer',
|
|
||||||
'dragleave',
|
'dragleave',
|
||||||
'dragleavespacer',
|
|
||||||
'dragover',
|
'dragover',
|
||||||
'dragoverspacer',
|
|
||||||
'drop',
|
'drop',
|
||||||
'input',
|
'input',
|
||||||
],
|
],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
ActionTile,
|
ActionTile,
|
||||||
Droppable,
|
ListItem,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
@ -117,94 +71,56 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
dragging: false,
|
dragging: false,
|
||||||
dragItem: undefined,
|
|
||||||
dropIndex: undefined,
|
|
||||||
newAction: {},
|
|
||||||
spacerElements: {},
|
|
||||||
visibleTopSpacers: {},
|
|
||||||
visibleBottomSpacers: {},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
componentsData() {
|
||||||
|
return {
|
||||||
|
on: {
|
||||||
|
dragend: this.onDragEnd,
|
||||||
|
dragover: this.onDragOver,
|
||||||
|
drop: this.onDrop,
|
||||||
|
input: this.onInput,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onDragStart(event) {
|
onDragStart(event) {
|
||||||
|
if (this.readOnly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.dragging = true
|
this.dragging = true
|
||||||
this.$emit('drag', event)
|
this.$emit('drag', event)
|
||||||
},
|
},
|
||||||
|
|
||||||
onDragEnd(event) {
|
onDragEnd(event) {
|
||||||
|
event.stopPropagation()
|
||||||
this.dragging = false
|
this.dragging = false
|
||||||
this.$emit('dragend', event)
|
this.$emit('dragend', event)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onDragOver(event) {
|
||||||
|
event.stopPropagation()
|
||||||
|
this.$emit('dragover', event)
|
||||||
|
},
|
||||||
|
|
||||||
onDrop(event) {
|
onDrop(event) {
|
||||||
|
if (this.readOnly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
this.dragging = false
|
this.dragging = false
|
||||||
this.$emit('drop', event)
|
this.$emit('drop', event)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onInput(value) {
|
||||||
|
this.$emit('input', value)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
$spacer-height: 1em;
|
|
||||||
|
|
||||||
.action {
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
.spacer {
|
|
||||||
height: $spacer-height;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
border-top: 1px dashed transparent;
|
|
||||||
border-bottom: 1px dashed transparent;
|
|
||||||
|
|
||||||
.droppable-frame {
|
|
||||||
border: 1px dashed $selected-fg;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
height: 5em;
|
|
||||||
padding: 0.5em 0;
|
|
||||||
|
|
||||||
.droppable-frame {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0.5em 0;
|
|
||||||
padding: 0.1em;
|
|
||||||
border: 2px dashed $ok-fg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.droppable {
|
|
||||||
height: 100%;
|
|
||||||
background: $play-btn-fg;
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
border-radius: 1em;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.droppable-wrapper,
|
|
||||||
.droppable-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.droppable-container {
|
|
||||||
.droppable-frame {
|
|
||||||
width: 100%;
|
|
||||||
height: 1px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.droppable {
|
|
||||||
width: 100%;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
<template>
|
||||||
|
<div class="add-tile-container">
|
||||||
|
<Tile class="add"
|
||||||
|
:draggable="false"
|
||||||
|
:read-only="true"
|
||||||
|
@click="$emit('click')">
|
||||||
|
<div class="add-tile">
|
||||||
|
<span class="icon">
|
||||||
|
<i :class="icon" />
|
||||||
|
</span>
|
||||||
|
<span class="name">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Tile>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Tile from "@/components/elements/Tile"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ['click'],
|
||||||
|
components: { Tile },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: 'fas fa-plus',
|
||||||
|
},
|
||||||
|
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "common";
|
||||||
|
|
||||||
|
.add-tile-container {
|
||||||
|
position: relative;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
margin-right: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,219 @@
|
||||||
|
<template>
|
||||||
|
<div class="condition-block">
|
||||||
|
<ActionsBlock :value="value"
|
||||||
|
:collapsed="collapsed"
|
||||||
|
:dragging="isDragging"
|
||||||
|
:has-else="hasElse"
|
||||||
|
:indent="indent"
|
||||||
|
:read-only="readOnly"
|
||||||
|
@input="onActionsChange"
|
||||||
|
@add-else="$emit('add-else')"
|
||||||
|
@drag="$emit('drag', $event)"
|
||||||
|
@dragend="$emit('dragend', $event)"
|
||||||
|
@dragenter="$emit('dragenter', $event)"
|
||||||
|
@dragleave="$emit('dragleave', $event)"
|
||||||
|
@dragover="$emit('dragover', $event)"
|
||||||
|
@drop="$emit('drop', $event)">
|
||||||
|
<template #before>
|
||||||
|
<ConditionTile :value="condition"
|
||||||
|
v-bind="conditionTileConf.props"
|
||||||
|
v-on="conditionTileConf.on"
|
||||||
|
@input.prevent.stop
|
||||||
|
:spacer-top="spacerTop"
|
||||||
|
:spacer-bottom="false"
|
||||||
|
v-if="condition && !isElse" />
|
||||||
|
|
||||||
|
<ConditionTile value="else"
|
||||||
|
v-bind="conditionTileConf.props"
|
||||||
|
v-on="conditionTileConf.on"
|
||||||
|
:is-else="true"
|
||||||
|
:spacer-top="spacerTop"
|
||||||
|
:spacer-bottom="false"
|
||||||
|
v-else-if="isElse" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #after>
|
||||||
|
<EndBlockTile value="end if"
|
||||||
|
icon="fas fa-question"
|
||||||
|
:active="active"
|
||||||
|
:spacer-bottom="spacerBottom"
|
||||||
|
@drop="onDrop"
|
||||||
|
v-if="isElse || !hasElse" />
|
||||||
|
</template>
|
||||||
|
</ActionsBlock>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ActionsBlock from "./ActionsBlock"
|
||||||
|
import ConditionTile from "./ConditionTile"
|
||||||
|
import EndBlockTile from "./EndBlockTile"
|
||||||
|
import Mixin from "./Mixin"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ConditionBlock',
|
||||||
|
mixins: [Mixin],
|
||||||
|
emits: [
|
||||||
|
'add-else',
|
||||||
|
'delete',
|
||||||
|
'drag',
|
||||||
|
'dragend',
|
||||||
|
'dragenter',
|
||||||
|
'dragleave',
|
||||||
|
'dragover',
|
||||||
|
'drop',
|
||||||
|
'input',
|
||||||
|
],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ActionsBlock,
|
||||||
|
ConditionTile,
|
||||||
|
EndBlockTile,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
collapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
dragging: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
hasElse: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
indent: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
isElse: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
spacerBottom: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
spacerTop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dragging_: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
condition() {
|
||||||
|
return this.getCondition(this.key)
|
||||||
|
},
|
||||||
|
|
||||||
|
conditionTileConf() {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
active: this.active,
|
||||||
|
readOnly: this.readOnly,
|
||||||
|
spacerBottom: this.spacerBottom,
|
||||||
|
spacerTop: this.spacerTop,
|
||||||
|
},
|
||||||
|
|
||||||
|
on: {
|
||||||
|
change: this.onConditionChange,
|
||||||
|
delete: (event) => this.$emit('delete', event),
|
||||||
|
drag: this.onDragStart,
|
||||||
|
dragend: this.onDragEnd,
|
||||||
|
dragenterspacer: (event) => this.$emit('dragenter', event),
|
||||||
|
dragleavespacer: (event) => this.$emit('dragleave', event),
|
||||||
|
dragover: (event) => this.$emit('dragover', event),
|
||||||
|
dragoverspacer: (event) => this.$emit('dragoverspacer', event),
|
||||||
|
drop: this.onDrop,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isDragging() {
|
||||||
|
return this.dragging_ || this.dragging
|
||||||
|
},
|
||||||
|
|
||||||
|
key() {
|
||||||
|
return this.getKey(this.value)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onActionsChange(value) {
|
||||||
|
if (!this.key || this.readOnly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('input', { [this.key]: value })
|
||||||
|
},
|
||||||
|
|
||||||
|
onConditionChange(condition) {
|
||||||
|
if (!this.key || this.readOnly || !condition?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
condition = `if \${${condition.trim()}}`
|
||||||
|
this.$emit('input', { [condition]: this.value[this.key] })
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragStart(event) {
|
||||||
|
if (this.readOnly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dragging_ = true
|
||||||
|
this.$emit('drag', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragEnd(event) {
|
||||||
|
this.dragging_ = false
|
||||||
|
this.$emit('dragend', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDrop(event) {
|
||||||
|
if (this.readOnly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dragging_ = false
|
||||||
|
this.$emit('drop', event)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.condition-block) {
|
||||||
|
.end-if-container {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<form class="condition-editor" @submit.prevent.stop="onSubmit">
|
||||||
|
<label for="condition">
|
||||||
|
Condition
|
||||||
|
<input type="text"
|
||||||
|
name="condition"
|
||||||
|
autocomplete="off"
|
||||||
|
:autofocus="true"
|
||||||
|
:value="value"
|
||||||
|
ref="text"
|
||||||
|
@input.stop="onInput" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<button type="submit" :disabled="!hasChanges">
|
||||||
|
<i class="fas fa-check" /> Save
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
emits: [
|
||||||
|
'input',
|
||||||
|
],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hasChanges: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onSubmit(event) {
|
||||||
|
const value = this.$refs.text.value.trim()
|
||||||
|
if (!value.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.target.value = value
|
||||||
|
this.$emit('input', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
onInput(event) {
|
||||||
|
const value = '' + event.target.value
|
||||||
|
if (!value?.trim()?.length) {
|
||||||
|
this.hasChanges = false
|
||||||
|
} else {
|
||||||
|
this.hasChanges = value !== this.value
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.text.value = value
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value() {
|
||||||
|
this.hasChanges = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.text.focus()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "common";
|
||||||
|
|
||||||
|
.condition-editor {
|
||||||
|
min-width: 40em;
|
||||||
|
max-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
label {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,216 @@
|
||||||
|
<template>
|
||||||
|
<ListItem class="condition-tile"
|
||||||
|
:value="value"
|
||||||
|
:active="active"
|
||||||
|
:read-only="readOnly"
|
||||||
|
:spacer-bottom="spacerBottom"
|
||||||
|
:spacer-top="spacerTop"
|
||||||
|
v-on="dragListeners"
|
||||||
|
@input="$emit('input', $event)">
|
||||||
|
<div class="drag-spacer" v-if="dragging && !spacerTop"> </div>
|
||||||
|
|
||||||
|
<Tile v-bind="tileConf.props"
|
||||||
|
v-on="tileConf.on"
|
||||||
|
:draggable="!readOnly"
|
||||||
|
@click.stop="showConditionEditor = true"
|
||||||
|
v-if="!isElse">
|
||||||
|
<div class="tile-name">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-question" />
|
||||||
|
</span>
|
||||||
|
<span class="name">
|
||||||
|
<span class="keyword">if</span> [
|
||||||
|
<span class="code" v-text="value" /> ]
|
||||||
|
<span class="keyword">then</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Tile>
|
||||||
|
|
||||||
|
<Tile v-bind="tileConf.props"
|
||||||
|
v-on="tileConf.on"
|
||||||
|
:draggable="false"
|
||||||
|
:read-only="true"
|
||||||
|
@click="$emit('click')"
|
||||||
|
v-else>
|
||||||
|
<div class="tile-name">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-question" />
|
||||||
|
</span>
|
||||||
|
<span class="name">
|
||||||
|
<span class="keyword">else</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Tile>
|
||||||
|
|
||||||
|
<div class="condition-editor-container" v-if="showConditionEditor && !readOnly">
|
||||||
|
<Modal title="Edit Condition"
|
||||||
|
:visible="true"
|
||||||
|
@close="showConditionEditor = false">
|
||||||
|
<ConditionEditor :value="value"
|
||||||
|
ref="conditionEditor"
|
||||||
|
@input.prevent.stop="onConditionChange"
|
||||||
|
v-if="showConditionEditor" />
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ConditionEditor from "./ConditionEditor"
|
||||||
|
import ListItem from "./ListItem"
|
||||||
|
import Modal from "@/components/Modal"
|
||||||
|
import Tile from "@/components/elements/Tile"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: [
|
||||||
|
'change',
|
||||||
|
'click',
|
||||||
|
'delete',
|
||||||
|
'drag',
|
||||||
|
'dragend',
|
||||||
|
'dragenter',
|
||||||
|
'dragleave',
|
||||||
|
'dragover',
|
||||||
|
'drop',
|
||||||
|
'input',
|
||||||
|
],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ConditionEditor,
|
||||||
|
ListItem,
|
||||||
|
Modal,
|
||||||
|
Tile,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
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: {
|
||||||
|
dragListeners() {
|
||||||
|
return this.readOnly ? {} : {
|
||||||
|
drag: this.onDragStart,
|
||||||
|
dragend: this.onDragEnd,
|
||||||
|
dragenter: (event) => this.$emit('dragenter', event),
|
||||||
|
dragleave: (event) => this.$emit('dragleave', event),
|
||||||
|
dragover: (event) => this.$emit('dragover', event),
|
||||||
|
drop: this.onDrop,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
tileConf() {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
value: this.value,
|
||||||
|
class: 'keyword',
|
||||||
|
readOnly: this.readOnly,
|
||||||
|
withDelete: !this.readOnly,
|
||||||
|
},
|
||||||
|
|
||||||
|
on: {
|
||||||
|
...this.dragListeners,
|
||||||
|
delete: () => this.$emit('delete'),
|
||||||
|
input: this.onInput,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dragging: false,
|
||||||
|
showConditionEditor: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onConditionChange(event) {
|
||||||
|
this.showConditionEditor = false
|
||||||
|
if (this.readOnly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const condition = event.target.value?.trim()
|
||||||
|
if (!condition?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.target.value = condition
|
||||||
|
this.$emit('change', condition)
|
||||||
|
},
|
||||||
|
|
||||||
|
onInput(value) {
|
||||||
|
if (!value || this.readOnly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('input', value)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragStart(event) {
|
||||||
|
if (this.readOnly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dragging = true
|
||||||
|
this.$emit('drag', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragEnd(event) {
|
||||||
|
this.dragging = false
|
||||||
|
this.$emit('dragend', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDrop(event) {
|
||||||
|
this.dragging = false
|
||||||
|
if (this.readOnly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('drop', event)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "common";
|
||||||
|
|
||||||
|
.action-tile {
|
||||||
|
.condition {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-spacer {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<ListItem class="end-block-container"
|
||||||
|
:active="active"
|
||||||
|
:value="{}"
|
||||||
|
:read-only="false"
|
||||||
|
:spacer-bottom="spacerBottom"
|
||||||
|
:spacer-top="spacerTop"
|
||||||
|
@dragenter="$emit('dragenter', $event)"
|
||||||
|
@dragleave="$emit('dragleave', $event)"
|
||||||
|
@dragover="$emit('dragover', $event)"
|
||||||
|
@drop="$emit('drop', $event)">
|
||||||
|
<Tile class="keyword" :draggable="false" :read-only="true">
|
||||||
|
<div class="tile-name">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-question" />
|
||||||
|
</span>
|
||||||
|
<span class="name">
|
||||||
|
<span class="keyword" v-text="value" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Tile>
|
||||||
|
</ListItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ListItem from "./ListItem"
|
||||||
|
import Mixin from "./Mixin"
|
||||||
|
import Tile from "@/components/elements/Tile"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [Mixin],
|
||||||
|
components: {
|
||||||
|
ListItem,
|
||||||
|
Tile,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
spacerTop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
spacerBottom: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
206
platypush/backend/http/webapp/src/components/Action/ListItem.vue
Normal file
206
platypush/backend/http/webapp/src/components/Action/ListItem.vue
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
<template>
|
||||||
|
<div class="row item list-item" :class="itemClass">
|
||||||
|
<div class="spacer-wrapper" :class="{ hidden: !spacerTop }">
|
||||||
|
<div class="spacer" :class="{ active }" ref="dropTargetTop">
|
||||||
|
<div class="droppable-wrapper">
|
||||||
|
<div class="droppable-container">
|
||||||
|
<div class="droppable-frame">
|
||||||
|
<div class="droppable" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Droppable :element="$refs.dropTargetTop" :disabled="readOnly" v-on="droppableData.top.on" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spacer" v-if="dragging" />
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<div class="spacer" v-if="dragging" />
|
||||||
|
|
||||||
|
<div class="spacer-wrapper" :class="{ hidden: !spacerBottom }">
|
||||||
|
<div class="spacer" :class="{ active }" ref="dropTargetBottom">
|
||||||
|
<div class="droppable-wrapper">
|
||||||
|
<div class="droppable-container">
|
||||||
|
<div class="droppable-frame">
|
||||||
|
<div class="droppable" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Droppable :element="$refs.dropTargetBottom" :disabled="readOnly" v-on="droppableData.bottom.on" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Droppable from "@/components/elements/Droppable"
|
||||||
|
import Utils from "@/Utils"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [Utils],
|
||||||
|
emits: [
|
||||||
|
'contextmenu',
|
||||||
|
'dragend',
|
||||||
|
'dragenter',
|
||||||
|
'dragleave',
|
||||||
|
'dragover',
|
||||||
|
'drop',
|
||||||
|
],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Droppable,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
className: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
dragging: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
spacerBottom: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
spacerTop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
value: {
|
||||||
|
type: [String, Object],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
droppableData() {
|
||||||
|
return ['bottom', 'top'].reduce((acc, key) => {
|
||||||
|
acc[key] = {
|
||||||
|
on: {
|
||||||
|
dragend: this.onDragEnd,
|
||||||
|
dragenter: this.onDragEnter,
|
||||||
|
dragleave: this.onDragLeave,
|
||||||
|
dragover: this.onDragOver,
|
||||||
|
drop: this.onDrop,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
itemClass() {
|
||||||
|
return {
|
||||||
|
dragging: this.dragging,
|
||||||
|
...(this.className?.trim ? { [this.className]: true } : (this.className || {})),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onDragEnd(event) {
|
||||||
|
this.$emit('dragend', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragEnter(event) {
|
||||||
|
this.$emit('dragenter', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragLeave(event) {
|
||||||
|
this.$emit('dragleave', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragOver(event) {
|
||||||
|
this.$emit('dragover', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDrop(event) {
|
||||||
|
this.$emit('drop', event)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$spacer-height: 1em;
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
height: $spacer-height;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-top: 1px dashed transparent;
|
||||||
|
border-bottom: 1px dashed transparent;
|
||||||
|
|
||||||
|
.droppable-frame {
|
||||||
|
border: 1px dashed $selected-fg;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
height: 5em;
|
||||||
|
padding: 0.5em 0;
|
||||||
|
|
||||||
|
.droppable-frame {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding: 0.1em;
|
||||||
|
border: 2px dashed $ok-fg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.droppable {
|
||||||
|
height: 100%;
|
||||||
|
background: $play-btn-fg;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 1em;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.droppable-wrapper,
|
||||||
|
.droppable-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.droppable-container {
|
||||||
|
.droppable-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.droppable {
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
getCondition(value) {
|
||||||
|
value = this.getKey(value) || value
|
||||||
|
return value?.trim?.()?.match(/^if\s*\$\{(.*)\}\s*$/i)?.[1]?.trim?.()
|
||||||
|
},
|
||||||
|
|
||||||
|
getKey(value) {
|
||||||
|
return this.isKeyword(value) ? Object.keys(value)[0] : null
|
||||||
|
},
|
||||||
|
|
||||||
|
isAction(value) {
|
||||||
|
return typeof value === 'object' && !Array.isArray(value) && (value.action || value.name)
|
||||||
|
},
|
||||||
|
|
||||||
|
isActionsBlock(value) {
|
||||||
|
return this.getCondition(value) || this.isElse(value)
|
||||||
|
},
|
||||||
|
|
||||||
|
isKeyword(value) {
|
||||||
|
return (
|
||||||
|
value &&
|
||||||
|
typeof value === 'object' &&
|
||||||
|
!Array.isArray(value) &&
|
||||||
|
Object.keys(value).length === 1 &&
|
||||||
|
!this.isAction(value)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
isElse(value) {
|
||||||
|
return (this.getKey(value) || value)?.toLowerCase?.()?.trim?.() === 'else'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -64,7 +64,7 @@
|
||||||
|
|
||||||
<ActionsList :value="newValue.actions"
|
<ActionsList :value="newValue.actions"
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
@input="newValue.actions = $event" />
|
@input="onActionsEdit" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Structured response container -->
|
<!-- Structured response container -->
|
||||||
|
@ -176,7 +176,7 @@
|
||||||
<FloatingButton icon-class="fa fa-save"
|
<FloatingButton icon-class="fa fa-save"
|
||||||
right glow
|
right glow
|
||||||
title="Save Procedure"
|
title="Save Procedure"
|
||||||
:disabled="!hasChanges"
|
:disabled="!canSave"
|
||||||
@click="save"
|
@click="save"
|
||||||
v-if="showSave" />
|
v-if="showSave" />
|
||||||
<FloatingButton icon-class="fa fa-play"
|
<FloatingButton icon-class="fa fa-play"
|
||||||
|
@ -292,14 +292,24 @@ export default {
|
||||||
return this.$el.querySelector('.floating-btns')
|
return this.$el.querySelector('.floating-btns')
|
||||||
},
|
},
|
||||||
|
|
||||||
hasChanges() {
|
canSave() {
|
||||||
if (!this.newValue?.name?.length)
|
if (
|
||||||
|
!this.withSave ||
|
||||||
|
this.readOnly ||
|
||||||
|
!this.newValue?.name?.length ||
|
||||||
|
!this.newValue?.actions?.length
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
if (!this.newValue?.actions?.length)
|
return this.valueString !== this.newValueString
|
||||||
return false
|
},
|
||||||
|
|
||||||
return JSON.stringify(this.value) !== JSON.stringify(this.newValue)
|
valueString() {
|
||||||
|
return JSON.stringify(this.value)
|
||||||
|
},
|
||||||
|
|
||||||
|
newValueString() {
|
||||||
|
return JSON.stringify(this.newValue)
|
||||||
},
|
},
|
||||||
|
|
||||||
modal_() {
|
modal_() {
|
||||||
|
@ -310,7 +320,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldConfirmClose() {
|
shouldConfirmClose() {
|
||||||
return this.hasChanges && !this.readOnly && this.withSave && !this.shouldForceClose
|
return this.canSave && !this.shouldForceClose
|
||||||
},
|
},
|
||||||
|
|
||||||
showArgs() {
|
showArgs() {
|
||||||
|
@ -324,7 +334,7 @@ export default {
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async save() {
|
async save() {
|
||||||
if (!this.hasChanges)
|
if (!this.canSave)
|
||||||
return
|
return
|
||||||
|
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
@ -468,12 +478,13 @@ export default {
|
||||||
|
|
||||||
duplicate() {
|
duplicate() {
|
||||||
const name = `${this.newValue.name || ''}__copy`
|
const name = `${this.newValue.name || ''}__copy`
|
||||||
|
const duplicate = JSON.parse(JSON.stringify(this.newValue))
|
||||||
this.duplicateValue = {
|
this.duplicateValue = {
|
||||||
...this.newValue,
|
...duplicate,
|
||||||
...{
|
...{
|
||||||
meta: {
|
meta: {
|
||||||
...(this.newValue.meta || {}),
|
...(duplicate.meta || {}),
|
||||||
icon: {...(this.newValue.meta?.icon || {})},
|
icon: {...(duplicate.meta?.icon || {})},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
id: null,
|
id: null,
|
||||||
|
@ -482,16 +493,8 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
editAction(action, index) {
|
onActionsEdit(actions) {
|
||||||
this.newValue.actions[index] = action
|
this.newValue.actions = actions
|
||||||
},
|
|
||||||
|
|
||||||
addAction(action) {
|
|
||||||
this.newValue.actions.push(action)
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteAction(index) {
|
|
||||||
this.newValue.actions.splice(index, 1)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onArgInput(arg, index) {
|
onArgInput(arg, index) {
|
||||||
|
@ -630,11 +633,12 @@ export default {
|
||||||
if (!this.value)
|
if (!this.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
const value = JSON.parse(JSON.stringify(this.value))
|
||||||
this.newValue = {
|
this.newValue = {
|
||||||
...this.value,
|
...value,
|
||||||
actions: this.value.actions?.map(a => ({...a})),
|
actions: value.actions?.map(a => ({...a})),
|
||||||
args: [...(this.value?.args || [])],
|
args: [...(value?.args || [])],
|
||||||
meta: {...(this.value?.meta || {})},
|
meta: {...(value?.meta || {})},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
175
platypush/backend/http/webapp/src/components/elements/Tile.vue
Normal file
175
platypush/backend/http/webapp/src/components/elements/Tile.vue
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
<template>
|
||||||
|
<div class="tile-container" :class="className">
|
||||||
|
<div class="tile" ref="tile" @click="$emit('click', $event)">
|
||||||
|
<div class="delete"
|
||||||
|
title="Remove"
|
||||||
|
v-if="withDelete"
|
||||||
|
@click.stop="$emit('delete')">
|
||||||
|
<i class="icon fas fa-xmark" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Draggable :element="tile"
|
||||||
|
:disabled="readOnly"
|
||||||
|
:value="value"
|
||||||
|
@drag="$emit('drag', $event)"
|
||||||
|
@drop="$emit('drop', $event)"
|
||||||
|
v-if="draggable" />
|
||||||
|
|
||||||
|
<Droppable :element="tile"
|
||||||
|
@dragenter="$emit('dragenter', $event)"
|
||||||
|
@dragleave="$emit('dragleave', $event)"
|
||||||
|
@dragover="$emit('dragover', $event)"
|
||||||
|
@drop="$emit('drop', $event)"
|
||||||
|
v-if="!readOnly" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Draggable from "@/components/elements/Draggable"
|
||||||
|
import Droppable from "@/components/elements/Droppable"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: [
|
||||||
|
'click',
|
||||||
|
'delete',
|
||||||
|
'drag',
|
||||||
|
'dragenter',
|
||||||
|
'dragleave',
|
||||||
|
'dragover',
|
||||||
|
'drop',
|
||||||
|
],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Draggable,
|
||||||
|
Droppable,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
className: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
draggable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
value: {
|
||||||
|
type: [Object, String, Number, Boolean, Array],
|
||||||
|
},
|
||||||
|
|
||||||
|
withDelete: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tile: undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.tile = this.$refs.tile
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tile-container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
min-width: 20em;
|
||||||
|
background: $tile-bg;
|
||||||
|
color: $tile-fg;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
content: "";
|
||||||
|
position: relative;
|
||||||
|
border-radius: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $tile-hover-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: $tile-hover-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
font-size: 1.25em;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.25em;
|
||||||
|
right: 0;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.25s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.keyword {
|
||||||
|
.tile {
|
||||||
|
background: $tile-bg-2;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $tile-hover-bg-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.add {
|
||||||
|
.tile {
|
||||||
|
background: $tile-bg-3;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $tile-hover-bg-3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.tile) {
|
||||||
|
.tile-name {
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: monospace;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
margin-right: 0.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword {
|
||||||
|
color: $tile-keyword-fg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
color: $tile-code-fg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -160,7 +160,12 @@ export default {
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
editIcon() {
|
editIcon() {
|
||||||
this.newIcon = (this.entity.meta?.icon?.['class'] || this.entity.meta?.icon?.url)?.trim()
|
this.newIcon = (
|
||||||
|
this.entity.meta?.icon?.url ||
|
||||||
|
this.entity.meta?.icon?.['class'] ||
|
||||||
|
this.currentIcon.url ||
|
||||||
|
this.currentIcon.class
|
||||||
|
)?.trim()
|
||||||
},
|
},
|
||||||
|
|
||||||
newIcon() {
|
newIcon() {
|
||||||
|
|
|
@ -133,6 +133,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="item">
|
||||||
|
<IconEditor :entity="value" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="item actions" v-if="value?.actions?.length">
|
<div class="item actions" v-if="value?.actions?.length">
|
||||||
<div class="label">Actions</div>
|
<div class="label">Actions</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
|
@ -212,6 +216,7 @@ import ConfirmDialog from "@/components/elements/ConfirmDialog";
|
||||||
import EntityMixin from "./EntityMixin"
|
import EntityMixin from "./EntityMixin"
|
||||||
import EntityIcon from "./EntityIcon"
|
import EntityIcon from "./EntityIcon"
|
||||||
import FileEditor from "@/components/File/EditorModal";
|
import FileEditor from "@/components/File/EditorModal";
|
||||||
|
import IconEditor from "@/components/panels/Entities/IconEditor";
|
||||||
import ProcedureEditor from "@/components/Procedure/ProcedureEditorModal"
|
import ProcedureEditor from "@/components/Procedure/ProcedureEditorModal"
|
||||||
import Response from "@/components/Action/Response"
|
import Response from "@/components/Action/Response"
|
||||||
|
|
||||||
|
@ -220,6 +225,7 @@ export default {
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
FileEditor,
|
FileEditor,
|
||||||
|
IconEditor,
|
||||||
ProcedureEditor,
|
ProcedureEditor,
|
||||||
Response,
|
Response,
|
||||||
},
|
},
|
||||||
|
@ -658,6 +664,8 @@ $icon-width: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
@include until($tablet) {
|
@include until($tablet) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue