[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>
|
||||
<div class="action-tile-container">
|
||||
<div class="action-tile"
|
||||
:class="{ new: isNew }"
|
||||
ref="tile"
|
||||
@click="$refs.actionEditor.show">
|
||||
<div class="action-delete"
|
||||
|
@ -122,6 +123,10 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
isNew() {
|
||||
return !this.readOnly && !this.name?.length
|
||||
},
|
||||
|
||||
name() {
|
||||
return this.value.name || this.value.action
|
||||
},
|
||||
|
@ -179,6 +184,14 @@ export default {
|
|||
background: $tile-hover-bg;
|
||||
}
|
||||
|
||||
&.new {
|
||||
background: $tile-bg-3;
|
||||
|
||||
&:hover {
|
||||
background: $tile-hover-bg-3;
|
||||
}
|
||||
}
|
||||
|
||||
.action-delete {
|
||||
width: 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>
|
||||
<div class="actions" :class="{dragging: dragItem != null}">
|
||||
<div class="row item action" v-for="(action, index) in actions" :key="index">
|
||||
<ActionsListItem :value="action"
|
||||
:active="dragItem != null"
|
||||
:read-only="readOnly"
|
||||
:spacer-bottom="visibleBottomSpacers[index]"
|
||||
: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 class="actions-list">
|
||||
<div class="indent-spacers" v-if="indent > 0">
|
||||
<div class="indent-spacer" @click="onCollapse">
|
||||
<div class="left side" />
|
||||
<div class="right side" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row item action">
|
||||
<ActionTile :value="newAction"
|
||||
:draggable="false"
|
||||
@input="addAction"
|
||||
v-if="!readOnly" />
|
||||
<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"
|
||||
:draggable="false"
|
||||
@input="addAction" />
|
||||
</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>
|
||||
</template>
|
||||
|
@ -30,17 +59,68 @@
|
|||
<script>
|
||||
import ActionsListItem from "./ActionsListItem"
|
||||
import ActionTile from "./ActionTile"
|
||||
import AddTile from "./AddTile"
|
||||
import ConditionBlock from "./ConditionBlock"
|
||||
import ListItem from "./ListItem"
|
||||
import Mixin from "./Mixin"
|
||||
import Utils from "@/Utils"
|
||||
|
||||
export default {
|
||||
mixins: [Utils],
|
||||
emits: ['input'],
|
||||
name: 'ActionsList',
|
||||
mixins: [Mixin, Utils],
|
||||
emits: [
|
||||
'add-else',
|
||||
'change',
|
||||
'collapse',
|
||||
'drag',
|
||||
'dragend',
|
||||
'dragenter',
|
||||
'dragleave',
|
||||
'dragover',
|
||||
'drop',
|
||||
'input',
|
||||
'reset',
|
||||
],
|
||||
|
||||
components: {
|
||||
ActionsListItem,
|
||||
ActionTile,
|
||||
AddTile,
|
||||
ConditionBlock,
|
||||
ListItem,
|
||||
},
|
||||
|
||||
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: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
|
@ -48,112 +128,399 @@ export default {
|
|||
actions: [],
|
||||
}),
|
||||
},
|
||||
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
actions: [],
|
||||
dragItem: undefined,
|
||||
dropIndex: undefined,
|
||||
newValue: [],
|
||||
dragIndices: undefined,
|
||||
initialValue: undefined,
|
||||
newAction: {},
|
||||
spacerElements: {},
|
||||
visibleTopSpacers: {},
|
||||
visibleBottomSpacers: {},
|
||||
}
|
||||
},
|
||||
|
||||
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() {
|
||||
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: {
|
||||
onDrop(index) {
|
||||
if (this.dragItem == null || this.readOnly)
|
||||
onDragStart(index, event) {
|
||||
if (this.readOnly)
|
||||
return
|
||||
|
||||
this.actions.splice(
|
||||
index, 0, this.actions.splice(this.dragItem, 1)[0]
|
||||
if (Array.isArray(event)) {
|
||||
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
|
||||
this.dropIndex = null
|
||||
// If the drop location is an else block, then the target needs to be the next
|
||||
// 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) {
|
||||
if (this.dragItem == null || this.readOnly || this.dragItem === index)
|
||||
return
|
||||
onCollapse() {
|
||||
this.$emit('collapse')
|
||||
},
|
||||
|
||||
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,
|
||||
getParentBlock(indices) {
|
||||
indices = [...indices]
|
||||
let parent = this.newValue
|
||||
while (parent && indices.length > 1) {
|
||||
parent = parent[indices.shift()]
|
||||
}
|
||||
|
||||
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
|
||||
if (parent) {
|
||||
const blockKey = this.getKey(parent)
|
||||
if (blockKey) {
|
||||
parent = parent[blockKey]
|
||||
}
|
||||
}
|
||||
|
||||
return parent
|
||||
},
|
||||
|
||||
editAction(action, index) {
|
||||
this.actions[index] = action
|
||||
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) {
|
||||
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) {
|
||||
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() {
|
||||
this.visibleTopSpacers = Object.keys(this.actions).reduce((acc, index) => {
|
||||
acc[index] = true
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
this.visibleBottomSpacers = {[this.actions.length - 1]: true}
|
||||
this.syncSpacers()
|
||||
const el = this.$refs[`action-tile-${index}`]?.[0]?.$el
|
||||
if (el) {
|
||||
el.classList.add('shrink')
|
||||
setTimeout(() => {
|
||||
el.classList.remove('shrink')
|
||||
this.newValue.splice(index, items)
|
||||
}, 300)
|
||||
} else {
|
||||
this.newValue.splice(index, items)
|
||||
}
|
||||
},
|
||||
|
||||
syncSpacers() {
|
||||
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]
|
||||
return acc
|
||||
}, {})
|
||||
|
@ -164,35 +531,21 @@ export default {
|
|||
if (!this.value || !this.hasChanges)
|
||||
return
|
||||
|
||||
this.actions = this.value
|
||||
this.newValue = this.value
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
actions: {
|
||||
newValue: {
|
||||
deep: true,
|
||||
handler(value) {
|
||||
this.$emit('input', value)
|
||||
this.resetSpacers()
|
||||
this.syncSpacers()
|
||||
},
|
||||
},
|
||||
|
||||
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()
|
||||
}
|
||||
dragIndices() {
|
||||
this.syncSpacers()
|
||||
},
|
||||
|
||||
value: {
|
||||
|
@ -206,17 +559,57 @@ export default {
|
|||
|
||||
mounted() {
|
||||
this.syncValue()
|
||||
this.resetSpacers()
|
||||
this.syncSpacers()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$spacer-height: 1em;
|
||||
.actions-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.actions {
|
||||
.action {
|
||||
margin: 0;
|
||||
.actions {
|
||||
flex: 1;
|
||||
|
||||
.action {
|
||||
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>
|
||||
|
|
|
@ -1,68 +1,25 @@
|
|||
<template>
|
||||
<div class="row item action" :class="{ dragging }">
|
||||
<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"
|
||||
@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" />
|
||||
|
||||
<ListItem class="action"
|
||||
:active="active"
|
||||
:dragging="dragging"
|
||||
:spacer-bottom="spacerBottom"
|
||||
:spacer-top="spacerTop"
|
||||
:value="value"
|
||||
v-on="componentsData.on">
|
||||
<ActionTile :value="value"
|
||||
:draggable="!readOnly"
|
||||
:read-only="readOnly"
|
||||
:with-delete="!readOnly"
|
||||
v-on="componentsData.on"
|
||||
@contextmenu="$emit('contextmenu', $event)"
|
||||
@drag="onDragStart"
|
||||
@dragend.prevent="onDragEnd"
|
||||
@dragover.prevent="$emit('dragover', $event)"
|
||||
@drop="onDrop"
|
||||
@input="$emit('input', $event)"
|
||||
@drag.stop="onDragStart"
|
||||
@delete="$emit('delete', $event)" />
|
||||
|
||||
<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>
|
||||
</ListItem>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ActionTile from "@/components/Action/ActionTile"
|
||||
import Droppable from "@/components/elements/Droppable"
|
||||
import ListItem from "@/components/Action/ListItem"
|
||||
import Utils from "@/Utils"
|
||||
|
||||
export default {
|
||||
|
@ -73,18 +30,15 @@ export default {
|
|||
'drag',
|
||||
'dragend',
|
||||
'dragenter',
|
||||
'dragenterspacer',
|
||||
'dragleave',
|
||||
'dragleavespacer',
|
||||
'dragover',
|
||||
'dragoverspacer',
|
||||
'drop',
|
||||
'input',
|
||||
],
|
||||
|
||||
components: {
|
||||
ActionTile,
|
||||
Droppable,
|
||||
ListItem,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -117,94 +71,56 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
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: {
|
||||
onDragStart(event) {
|
||||
if (this.readOnly) {
|
||||
return
|
||||
}
|
||||
|
||||
this.dragging = true
|
||||
this.$emit('drag', event)
|
||||
},
|
||||
|
||||
onDragEnd(event) {
|
||||
event.stopPropagation()
|
||||
this.dragging = false
|
||||
this.$emit('dragend', event)
|
||||
},
|
||||
|
||||
onDragOver(event) {
|
||||
event.stopPropagation()
|
||||
this.$emit('dragover', event)
|
||||
},
|
||||
|
||||
onDrop(event) {
|
||||
if (this.readOnly) {
|
||||
return
|
||||
}
|
||||
|
||||
event.stopPropagation()
|
||||
this.dragging = false
|
||||
this.$emit('drop', event)
|
||||
},
|
||||
|
||||
onInput(value) {
|
||||
this.$emit('input', value)
|
||||
},
|
||||
},
|
||||
}
|
||||
</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"
|
||||
:read-only="readOnly"
|
||||
@input="newValue.actions = $event" />
|
||||
@input="onActionsEdit" />
|
||||
</div>
|
||||
|
||||
<!-- Structured response container -->
|
||||
|
@ -176,7 +176,7 @@
|
|||
<FloatingButton icon-class="fa fa-save"
|
||||
right glow
|
||||
title="Save Procedure"
|
||||
:disabled="!hasChanges"
|
||||
:disabled="!canSave"
|
||||
@click="save"
|
||||
v-if="showSave" />
|
||||
<FloatingButton icon-class="fa fa-play"
|
||||
|
@ -292,14 +292,24 @@ export default {
|
|||
return this.$el.querySelector('.floating-btns')
|
||||
},
|
||||
|
||||
hasChanges() {
|
||||
if (!this.newValue?.name?.length)
|
||||
canSave() {
|
||||
if (
|
||||
!this.withSave ||
|
||||
this.readOnly ||
|
||||
!this.newValue?.name?.length ||
|
||||
!this.newValue?.actions?.length
|
||||
)
|
||||
return false
|
||||
|
||||
if (!this.newValue?.actions?.length)
|
||||
return false
|
||||
return this.valueString !== this.newValueString
|
||||
},
|
||||
|
||||
return JSON.stringify(this.value) !== JSON.stringify(this.newValue)
|
||||
valueString() {
|
||||
return JSON.stringify(this.value)
|
||||
},
|
||||
|
||||
newValueString() {
|
||||
return JSON.stringify(this.newValue)
|
||||
},
|
||||
|
||||
modal_() {
|
||||
|
@ -310,7 +320,7 @@ export default {
|
|||
},
|
||||
|
||||
shouldConfirmClose() {
|
||||
return this.hasChanges && !this.readOnly && this.withSave && !this.shouldForceClose
|
||||
return this.canSave && !this.shouldForceClose
|
||||
},
|
||||
|
||||
showArgs() {
|
||||
|
@ -324,7 +334,7 @@ export default {
|
|||
|
||||
methods: {
|
||||
async save() {
|
||||
if (!this.hasChanges)
|
||||
if (!this.canSave)
|
||||
return
|
||||
|
||||
this.loading = true
|
||||
|
@ -468,12 +478,13 @@ export default {
|
|||
|
||||
duplicate() {
|
||||
const name = `${this.newValue.name || ''}__copy`
|
||||
const duplicate = JSON.parse(JSON.stringify(this.newValue))
|
||||
this.duplicateValue = {
|
||||
...this.newValue,
|
||||
...duplicate,
|
||||
...{
|
||||
meta: {
|
||||
...(this.newValue.meta || {}),
|
||||
icon: {...(this.newValue.meta?.icon || {})},
|
||||
...(duplicate.meta || {}),
|
||||
icon: {...(duplicate.meta?.icon || {})},
|
||||
}
|
||||
},
|
||||
id: null,
|
||||
|
@ -482,16 +493,8 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
editAction(action, index) {
|
||||
this.newValue.actions[index] = action
|
||||
},
|
||||
|
||||
addAction(action) {
|
||||
this.newValue.actions.push(action)
|
||||
},
|
||||
|
||||
deleteAction(index) {
|
||||
this.newValue.actions.splice(index, 1)
|
||||
onActionsEdit(actions) {
|
||||
this.newValue.actions = actions
|
||||
},
|
||||
|
||||
onArgInput(arg, index) {
|
||||
|
@ -630,11 +633,12 @@ export default {
|
|||
if (!this.value)
|
||||
return
|
||||
|
||||
const value = JSON.parse(JSON.stringify(this.value))
|
||||
this.newValue = {
|
||||
...this.value,
|
||||
actions: this.value.actions?.map(a => ({...a})),
|
||||
args: [...(this.value?.args || [])],
|
||||
meta: {...(this.value?.meta || {})},
|
||||
...value,
|
||||
actions: value.actions?.map(a => ({...a})),
|
||||
args: [...(value?.args || [])],
|
||||
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: {
|
||||
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() {
|
||||
|
|
|
@ -133,6 +133,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<IconEditor :entity="value" />
|
||||
</div>
|
||||
|
||||
<div class="item actions" v-if="value?.actions?.length">
|
||||
<div class="label">Actions</div>
|
||||
<div class="value">
|
||||
|
@ -212,6 +216,7 @@ import ConfirmDialog from "@/components/elements/ConfirmDialog";
|
|||
import EntityMixin from "./EntityMixin"
|
||||
import EntityIcon from "./EntityIcon"
|
||||
import FileEditor from "@/components/File/EditorModal";
|
||||
import IconEditor from "@/components/panels/Entities/IconEditor";
|
||||
import ProcedureEditor from "@/components/Procedure/ProcedureEditorModal"
|
||||
import Response from "@/components/Action/Response"
|
||||
|
||||
|
@ -220,6 +225,7 @@ export default {
|
|||
ConfirmDialog,
|
||||
EntityIcon,
|
||||
FileEditor,
|
||||
IconEditor,
|
||||
ProcedureEditor,
|
||||
Response,
|
||||
},
|
||||
|
@ -658,6 +664,8 @@ $icon-width: 2em;
|
|||
}
|
||||
|
||||
.value {
|
||||
text-align: right;
|
||||
|
||||
@include until($tablet) {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue