diff --git a/platypush/backend/http/webapp/src/components/Action/ActionsBlock.vue b/platypush/backend/http/webapp/src/components/Action/ActionsBlock.vue index d5f508487..315bc46a6 100644 --- a/platypush/backend/http/webapp/src/components/Action/ActionsBlock.vue +++ b/platypush/backend/http/webapp/src/components/Action/ActionsBlock.vue @@ -14,6 +14,7 @@ :dragging="dragging" :has-else="hasElse" :indent="indent" + :is-inside-loop="isInsideLoop" :parent="value" :read-only="readOnly" @add-else="$emit('add-else')" @@ -84,6 +85,11 @@ export default { default: 0, }, + isInsideLoop: { + type: Boolean, + default: false, + }, + readOnly: { type: Boolean, default: false, diff --git a/platypush/backend/http/webapp/src/components/Action/ActionsList.vue b/platypush/backend/http/webapp/src/components/Action/ActionsList.vue index d366e87f6..4efbb453f 100644 --- a/platypush/backend/http/webapp/src/components/Action/ActionsList.vue +++ b/platypush/backend/http/webapp/src/components/Action/ActionsList.vue @@ -25,6 +25,24 @@ :is-else="true" v-else-if="elses[index]" /> + <LoopBlock v-bind="componentsData[index].props" + v-on="componentsData[index].on" + :collapsed="collapsedBlocks[index]" + :dragging="isDragging" + v-else-if="fors[index]" /> + + <BreakTile :active="isDragging" + :readOnly="readOnly" + :spacerTop="componentsData[index].props.spacerTop" + @delete="deleteAction(index)" + v-else-if="isBreak(action)" /> + + <ContinueTile :active="isDragging" + :readOnly="readOnly" + :spacerTop="componentsData[index].props.spacerTop" + @delete="deleteAction(index)" + v-else-if="isContinue(action)" /> + <ReturnTile v-bind="componentsData[index].props" :value="returnValue" @change="editReturn($event)" @@ -67,6 +85,18 @@ <div class="row item action add-else-container" v-if="visibleAddButtons.else"> <AddTile icon="fas fa-question" title="Add Else" @click="$emit('add-else')" /> </div> + + <div class="row item action add-for-container" v-if="visibleAddButtons.loop"> + <AddTile icon="fas fa-arrow-rotate-left" title="Add Loop" @click="addLoop" /> + </div> + + <div class="row item action add-break-container" v-if="visibleAddButtons.break"> + <AddTile icon="fas fa-hand" title="Add Break" @click="addBreak" /> + </div> + + <div class="row item action add-continue-container" v-if="visibleAddButtons.continue"> + <AddTile icon="fas fa-rotate" title="Add Continue" @click="addContinue" /> + </div> </div> </div> </div> @@ -76,8 +106,11 @@ import ActionsListItem from "./ActionsListItem" import ActionTile from "./ActionTile" import AddTile from "./AddTile" +import BreakTile from "./BreakTile" import ConditionBlock from "./ConditionBlock" +import ContinueTile from "./ContinueTile" import ListItem from "./ListItem" +import LoopBlock from "./LoopBlock" import Mixin from "./Mixin" import ReturnTile from "./ReturnTile" import Utils from "@/Utils" @@ -103,8 +136,11 @@ export default { ActionsListItem, ActionTile, AddTile, + BreakTile, ConditionBlock, + ContinueTile, ListItem, + LoopBlock, ReturnTile, }, @@ -119,12 +155,17 @@ export default { default: false, }, + hasElse: { + type: Boolean, + default: false, + }, + indent: { type: Number, default: 0, }, - hasElse: { + isInsideLoop: { type: Boolean, default: false, }, @@ -187,6 +228,7 @@ export default { props: { value: action, active: this.isDragging, + isInsideLoop: !!(this.isInsideLoop || this.getFor(action) || this.getWhile(action)), readOnly: this.readOnly, ref: `action-tile-${index}`, spacerBottom: this.visibleBottomSpacers[index], @@ -223,6 +265,11 @@ export default { data.props.indent = this.indent + 1 } + const loop = this.getFor(action) + if (loop) { + data.props.async = loop.async + } + return data }) }, @@ -281,6 +328,16 @@ export default { }, {}) || {} }, + fors() { + return this.newValue?.reduce?.((acc, action, index) => { + if (this.getFor(action)) { + acc[index] = action + } + + return acc + }, {}) || {} + }, + hasChanges() { return this.newStringValue !== this.stringValue }, @@ -301,18 +358,16 @@ export default { return this.dragIndices?.[0] }, + breakIndex() { + return this.getTileIndex((action) => this.isBreak(action)) + }, + + continueIndex() { + return this.getTileIndex((action) => this.isContinue(action)) + }, + returnIndex() { - const ret = this.newValue?.reduce?.((acc, action, index) => { - if (acc >= 0) - return acc - - if (this.isReturn(action)) - return index - - return acc - }, -1) - - return ret >= 0 ? ret : null + return this.getTileIndex((action) => this.isReturn(action)) }, returnValue() { @@ -349,7 +404,22 @@ export default { }, stopIndex() { - return this.returnIndex + if (this.breakIndex != null) + return this.breakIndex + if (this.continueIndex != null) + return this.continueIndex + if (this.returnIndex != null) + return this.returnIndex + + return null + }, + + allowAddButtons() { + return ( + !this.readOnly && + !this.collapsed && + this.stopIndex == null + ) }, visibleActions() { @@ -360,8 +430,11 @@ export default { if ( this.conditions[index] || this.elses[index] || + this.fors[index] || this.isAction(action) || - this.isReturn(action) + this.isReturn(action) || + this.isBreak(action) || + this.isContinue(action) ) { acc[index] = action } @@ -372,16 +445,23 @@ export default { visibleAddButtons() { return { - action: !this.readOnly && !this.collapsed && this.stopIndex == null, - return: !this.readOnly && !this.collapsed && this.stopIndex == null, - condition: !this.readOnly && !this.collapsed && this.stopIndex == null, + action: this.allowAddButtons, + return: this.allowAddButtons, + condition: this.allowAddButtons, + loop: this.allowAddButtons, else: ( - !this.readOnly && - !this.collapsed && + this.allowAddButtons && this.parent && this.getCondition(this.parent) && - !this.hasElse && - this.stopIndex == null + !this.hasElse + ), + break: ( + this.allowAddButtons && + this.isInsideLoop + ), + continue: ( + this.allowAddButtons && + this.isInsideLoop ), } }, @@ -583,6 +663,19 @@ export default { this.selectLastExprEditor() }, + addLoop() { + this.newValue.push({ 'for _ in ${range(10)}': [] }) + this.selectLastExprEditor() + }, + + addBreak() { + this.newValue.push('break') + }, + + addContinue() { + this.newValue.push('continue') + }, + addReturn() { this.newValue.push({ 'return': null }) this.selectLastExprEditor() @@ -606,7 +699,7 @@ export default { newTileElement.click() this.$nextTick(() => { - const exprEditor = newTile.$el?.querySelector('.expr-editor-container') + const exprEditor = newTile.$el?.querySelector('.editor-container') if (!exprEditor) { return } @@ -644,6 +737,20 @@ export default { } }, + getTileIndex(callback) { + const ret = this.newValue?.reduce?.((acc, action, index) => { + if (acc >= 0) + return acc + + if (callback(action)) + return index + + return acc + }, -1) + + return ret >= 0 ? ret : null + }, + syncSpacers() { this.$nextTick(() => { this.spacerElements = Object.keys(this.newValue).reduce((acc, index) => { diff --git a/platypush/backend/http/webapp/src/components/Action/BreakTile.vue b/platypush/backend/http/webapp/src/components/Action/BreakTile.vue new file mode 100644 index 000000000..4c4fc1cc8 --- /dev/null +++ b/platypush/backend/http/webapp/src/components/Action/BreakTile.vue @@ -0,0 +1,66 @@ +<template> + <ListItem class="break-tile" + value="break" + :active="active" + :read-only="readOnly" + :spacer-bottom="spacerBottom" + :spacer-top="spacerTop"> + <Tile :value="value" + class="keyword" + :draggable="false" + :read-only="readOnly" + :with-delete="!readOnly" + @click.stop + @delete="$emit('delete')"> + <div class="tile-name"> + <span class="icon"> + <i class="fas fa-hand" /> + </span> + <span class="name"> + <span class="keyword">break</span> + </span> + </div> + </Tile> + </ListItem> +</template> + +<script> +import ListItem from "./ListItem" +import Tile from "@/components/elements/Tile" + +export default { + emits: ['delete'], + + components: { + ListItem, + Tile, + }, + + props: { + value: { + type: String, + default: 'break', + }, + + active: { + type: Boolean, + default: false, + }, + + readOnly: { + type: Boolean, + default: false, + }, + + spacerBottom: { + type: Boolean, + default: true, + }, + + spacerTop: { + type: Boolean, + default: true, + }, + }, +} +</script> diff --git a/platypush/backend/http/webapp/src/components/Action/ConditionBlock.vue b/platypush/backend/http/webapp/src/components/Action/ConditionBlock.vue index d0f6f6e0c..40fb29189 100644 --- a/platypush/backend/http/webapp/src/components/Action/ConditionBlock.vue +++ b/platypush/backend/http/webapp/src/components/Action/ConditionBlock.vue @@ -4,6 +4,7 @@ :collapsed="collapsed" :dragging="isDragging" :has-else="hasElse" + :is-inside-loop="isInsideLoop" :indent="indent" :read-only="readOnly" @input="onActionsChange" @@ -107,6 +108,11 @@ export default { default: false, }, + isInsideLoop: { + type: Boolean, + default: false, + }, + readOnly: { type: Boolean, default: false, diff --git a/platypush/backend/http/webapp/src/components/Action/ConditionTile.vue b/platypush/backend/http/webapp/src/components/Action/ConditionTile.vue index 4493b34c3..f33f7c7cb 100644 --- a/platypush/backend/http/webapp/src/components/Action/ConditionTile.vue +++ b/platypush/backend/http/webapp/src/components/Action/ConditionTile.vue @@ -21,7 +21,6 @@ <span class="name"> <span class="keyword">if</span> [ <span class="code" v-text="value" /> ] - <span class="keyword">then</span> </span> </div> </Tile> diff --git a/platypush/backend/http/webapp/src/components/Action/ContinueTile.vue b/platypush/backend/http/webapp/src/components/Action/ContinueTile.vue new file mode 100644 index 000000000..1df7dc865 --- /dev/null +++ b/platypush/backend/http/webapp/src/components/Action/ContinueTile.vue @@ -0,0 +1,66 @@ +<template> + <ListItem class="continue-tile" + value="continue" + :active="active" + :read-only="readOnly" + :spacer-bottom="spacerBottom" + :spacer-top="spacerTop"> + <Tile :value="value" + class="keyword" + :draggable="false" + :read-only="readOnly" + :with-delete="!readOnly" + @click.stop + @delete="$emit('delete')"> + <div class="tile-name"> + <span class="icon"> + <i class="fas fa-rotate" /> + </span> + <span class="name"> + <span class="keyword">continue</span> + </span> + </div> + </Tile> + </ListItem> +</template> + +<script> +import ListItem from "./ListItem" +import Tile from "@/components/elements/Tile" + +export default { + emits: ['delete'], + + components: { + ListItem, + Tile, + }, + + props: { + value: { + type: String, + default: 'continue', + }, + + active: { + type: Boolean, + default: false, + }, + + readOnly: { + type: Boolean, + default: false, + }, + + spacerBottom: { + type: Boolean, + default: true, + }, + + spacerTop: { + type: Boolean, + default: true, + }, + }, +} +</script> diff --git a/platypush/backend/http/webapp/src/components/Action/EndBlockTile.vue b/platypush/backend/http/webapp/src/components/Action/EndBlockTile.vue index 4ae3cee4a..5e3f56f5b 100644 --- a/platypush/backend/http/webapp/src/components/Action/EndBlockTile.vue +++ b/platypush/backend/http/webapp/src/components/Action/EndBlockTile.vue @@ -12,7 +12,7 @@ <Tile class="keyword" :draggable="false" :read-only="true"> <div class="tile-name"> <span class="icon"> - <i class="fas fa-question" /> + <i :class="icon" /> </span> <span class="name"> <span class="keyword" v-text="value" /> diff --git a/platypush/backend/http/webapp/src/components/Action/ExpressionEditor.vue b/platypush/backend/http/webapp/src/components/Action/ExpressionEditor.vue index 9b1c71ee4..bdf9b9661 100644 --- a/platypush/backend/http/webapp/src/components/Action/ExpressionEditor.vue +++ b/platypush/backend/http/webapp/src/components/Action/ExpressionEditor.vue @@ -29,7 +29,7 @@ export default { props: { value: { - type: String, + type: [String, Number, Boolean, Object, Array], default: '', }, @@ -83,7 +83,7 @@ export default { mounted() { this.hasChanges = false - if (!this.value?.trim()?.length) { + if (!this.value?.trim?.()?.length) { this.hasChanges = this.allowEmpty } diff --git a/platypush/backend/http/webapp/src/components/Action/ListItem.vue b/platypush/backend/http/webapp/src/components/Action/ListItem.vue index f9354f869..e938f5b18 100644 --- a/platypush/backend/http/webapp/src/components/Action/ListItem.vue +++ b/platypush/backend/http/webapp/src/components/Action/ListItem.vue @@ -87,7 +87,7 @@ export default { }, value: { - type: [String, Object], + type: [String, Number, Boolean, Object, Array], required: true, }, }, diff --git a/platypush/backend/http/webapp/src/components/Action/LoopBlock.vue b/platypush/backend/http/webapp/src/components/Action/LoopBlock.vue new file mode 100644 index 000000000..56c5ecdc7 --- /dev/null +++ b/platypush/backend/http/webapp/src/components/Action/LoopBlock.vue @@ -0,0 +1,204 @@ +<template> + <div class="loop-block"> + <ActionsBlock :value="value" + :collapsed="collapsed" + :dragging="isDragging" + :indent="indent" + :is-inside-loop="true" + :read-only="readOnly" + @input="onActionsChange" + @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> + <LoopTile v-bind="loopTileConf.props" + v-on="loopTileConf.on" + @input.prevent.stop + :spacer-top="spacerTop" + :spacer-bottom="false" /> + </template> + + <template #after> + <EndBlockTile value="end for" + icon="fas fa-arrow-rotate-right" + :active="active" + :spacer-bottom="spacerBottom" + @drop="onDrop" /> + </template> + </ActionsBlock> + </div> +</template> + +<script> +import ActionsBlock from "./ActionsBlock" +import EndBlockTile from "./EndBlockTile" +import LoopTile from "./LoopTile" +import Mixin from "./Mixin" + +export default { + name: 'LoopBlock', + mixins: [Mixin], + emits: [ + 'delete', + 'drag', + 'dragend', + 'dragenter', + 'dragleave', + 'dragover', + 'drop', + 'input', + ], + + components: { + ActionsBlock, + LoopTile, + EndBlockTile, + }, + + props: { + value: { + type: Object, + required: true, + }, + + active: { + type: Boolean, + default: false, + }, + + async: { + type: Boolean, + default: false, + }, + + collapsed: { + type: Boolean, + default: false, + }, + + dragging: { + type: Boolean, + default: false, + }, + + indent: { + type: Number, + default: 0, + }, + + isInsideLoop: { + type: Boolean, + default: true, + }, + + readOnly: { + type: Boolean, + default: false, + }, + + spacerBottom: { + type: Boolean, + default: false, + }, + + spacerTop: { + type: Boolean, + default: false, + }, + }, + + data() { + return { + dragging_: false, + } + }, + + computed: { + loop() { + return this.getFor(this.key) + }, + + loopTileConf() { + return { + props: { + ...this.loop, + active: this.active, + readOnly: this.readOnly, + spacerBottom: this.spacerBottom, + spacerTop: this.spacerTop, + }, + + on: { + change: this.onLoopChange, + 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 }) + }, + + onLoopChange(loop) { + const iterable = loop?.iterable?.trim() + const iterator = loop?.iterator?.trim() + const async_ = loop?.async || false + + if (!this.key || this.readOnly || !iterable?.length || !iterator?.length) { + return + } + + const keyword = 'for' + (async_ ? 'k' : '') + loop = `${keyword} ${iterator} in \${${iterable}}` + this.$emit('input', { [loop]: 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> diff --git a/platypush/backend/http/webapp/src/components/Action/LoopEditor.vue b/platypush/backend/http/webapp/src/components/Action/LoopEditor.vue new file mode 100644 index 000000000..d54803a7d --- /dev/null +++ b/platypush/backend/http/webapp/src/components/Action/LoopEditor.vue @@ -0,0 +1,153 @@ +<template> + <form class="loop-editor" @submit.prevent.stop="onSubmit"> + for + <label for="iterator"> + <input type="text" + name="iterator" + autocomplete="off" + :autofocus="true" + placeholder="Iterator" + :value="iterator" + ref="iterator" + @input.stop="onInput('iterator', $event)" /> + </label> + + in + + <label for="iterable"> + <input type="text" + name="iterable" + autocomplete="off" + :autofocus="true" + placeholder="Iterable" + :value="iterable" + ref="iterable" + @input.stop="onInput('iterable', $event)" /> + </label> + + <label class="async"> + <input class="checkbox" + type="checkbox" + name="async" + :checked="async" + @input.stop="onInput('async', $event)" /> + Run in parallel + </label> + + <label> + <button type="submit" :disabled="!hasChanges"> + <i class="fas fa-check" /> Save + </button> + </label> + </form> +</template> + +<script> +export default { + emits: [ + 'change', + 'input', + ], + + props: { + async: { + type: Boolean, + default: false, + }, + + iterable: { + type: String, + default: '', + }, + + iterator: { + type: String, + default: '', + }, + }, + + data() { + return { + hasChanges: true, + } + }, + + methods: { + onSubmit() { + const iterator = this.$refs.iterator.value.trim() + const iterable = this.$refs.iterable.value.trim() + if (!iterator.length || !iterable.length) { + return + } + + this.$emit('change', { iterator, iterable }) + }, + + onInput(target, event) { + const value = '' + event.target.value + if (!value?.trim()?.length) { + this.hasChanges = false + } else { + if (target === 'iterator') { + this.hasChanges = value !== this.iterator + } + + if (!this.hasChanges && target === 'iterable') { + this.hasChanges = value !== this.iterable + } + + if (!this.hasChanges && target === 'async') { + this.hasChanges = value !== this.async + } + } + + this.$nextTick(() => { + event.target.value = value + }) + }, + }, + + watch: { + value() { + this.hasChanges = false + }, + }, + + mounted() { + this.$nextTick(() => { + this.$refs.iterator.focus() + }) + }, +} +</script> + +<style lang="scss" scoped> +@import "common"; + +.loop-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; + } + + &.async { + justify-content: flex-start; + padding-bottom: 0; + } + } +} +</style> diff --git a/platypush/backend/http/webapp/src/components/Action/LoopTile.vue b/platypush/backend/http/webapp/src/components/Action/LoopTile.vue new file mode 100644 index 000000000..475bede10 --- /dev/null +++ b/platypush/backend/http/webapp/src/components/Action/LoopTile.vue @@ -0,0 +1,212 @@ +<template> + <ListItem class="loop-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="showLoopEditor = true"> + <div class="tile-name"> + <span class="icon"> + <i class="fas fa-arrow-rotate-left" /> + </span> + <span class="name"> + <span class="keyword">for<span v-if="async">k</span></span> + <span class="code" v-text="iterator" /> + <span class="keyword"> in </span> [ + <span class="code" v-text="iterable" /> ] + </span> + </div> + </Tile> + + <div class="editor-container" v-if="showLoopEditor && !readOnly"> + <Modal title="Edit Loop" + :visible="true" + @close="showLoopEditor = false"> + <LoopEditor :iterator="iterator" + :iterable="iterable" + :async="async" + ref="loopEditor" + @change="onLoopChange" + v-if="showLoopEditor"> + Loop + </LoopEditor> + </Modal> + </div> + </ListItem> +</template> + +<script> +import ListItem from "./ListItem" +import LoopEditor from "./LoopEditor" +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: { + LoopEditor, + ListItem, + Modal, + Tile, + }, + + props: { + iterator: { + type: String, + required: true, + }, + + iterable: { + type: String, + required: true, + }, + + active: { + type: Boolean, + default: false, + }, + + async: { + 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, + }, + } + }, + + value() { + return `for ${this.iterator} in ${this.iterable}` + }, + }, + + data() { + return { + dragging: false, + showLoopEditor: false, + } + }, + + methods: { + onLoopChange(event) { + this.showLoopEditor = false + if (this.readOnly) { + return + } + + this.$emit('change', event) + }, + + 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> diff --git a/platypush/backend/http/webapp/src/components/Action/Mixin.vue b/platypush/backend/http/webapp/src/components/Action/Mixin.vue index 4911c9a3d..e4f2cc196 100644 --- a/platypush/backend/http/webapp/src/components/Action/Mixin.vue +++ b/platypush/backend/http/webapp/src/components/Action/Mixin.vue @@ -6,16 +6,42 @@ export default { return value?.trim?.()?.match(/^if\s*\$\{(.*)\}\s*$/i)?.[1]?.trim?.() }, + getFor(value) { + value = this.getKey(value) || value + let m = value?.trim?.()?.match(/^for(k?)\s*(.*)\s*in\s*\$\{(.*)\}\s*$/i) + if (!m) + return null + + return { + async: m[1].length > 0, + iterator: m[2].trim(), + iterable: m[3].trim(), + } + }, + getKey(value) { return this.isKeyword(value) ? Object.keys(value)[0] : null }, + getWhile(value) { + value = this.getKey(value) || value + return value?.trim?.()?.match(/^while\s*\$\{(.*)\}\s*$/i)?.[1]?.trim?.() + }, + isAction(value) { return typeof value === 'object' && !Array.isArray(value) && (value.action || value.name) }, isActionsBlock(value) { - return this.getCondition(value) || this.isElse(value) + return this.getCondition(value) || this.isElse(value) || this.getFor(value) + }, + + isBreak(value) { + return value?.toLowerCase?.()?.trim?.() === 'break' + }, + + isContinue(value) { + return value?.toLowerCase?.()?.trim?.() === 'continue' }, isKeyword(value) { diff --git a/platypush/backend/http/webapp/src/components/Action/Response.vue b/platypush/backend/http/webapp/src/components/Action/Response.vue index 55ff647ff..93f63b96b 100644 --- a/platypush/backend/http/webapp/src/components/Action/Response.vue +++ b/platypush/backend/http/webapp/src/components/Action/Response.vue @@ -32,7 +32,7 @@ export default { mixins: [Utils], props: { response: { - type: [String, Object], + type: [String, Object, Array, Number, Boolean], }, error: { @@ -51,7 +51,7 @@ export default { jsonResponse() { if (this.isJSON) { - return hljs.highlight(this.response, {language: 'json'}).value + return hljs.highlight(this.response.toString(), {language: 'json'}).value } return null diff --git a/platypush/backend/http/webapp/src/components/Action/ReturnTile.vue b/platypush/backend/http/webapp/src/components/Action/ReturnTile.vue index a4fce9f21..cd9134c27 100644 --- a/platypush/backend/http/webapp/src/components/Action/ReturnTile.vue +++ b/platypush/backend/http/webapp/src/components/Action/ReturnTile.vue @@ -19,7 +19,7 @@ </div> </Tile> - <div class="expr-editor-container" v-if="showExprEditor && !readOnly"> + <div class="editor-container" v-if="showExprEditor && !readOnly"> <Modal title="Edit Return" :visible="true" @close="showExprEditor = false"> @@ -59,7 +59,7 @@ export default { props: { value: { - type: String, + type: [String, Number, Boolean, Object, Array], default: '', }, @@ -68,11 +68,6 @@ export default { default: false, }, - isElse: { - type: Boolean, - default: false, - }, - readOnly: { type: Boolean, default: false, diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/Procedure.vue b/platypush/backend/http/webapp/src/components/panels/Entities/Procedure.vue index 69dac41ca..f02715c96 100644 --- a/platypush/backend/http/webapp/src/components/panels/Entities/Procedure.vue +++ b/platypush/backend/http/webapp/src/components/panels/Entities/Procedure.vue @@ -126,7 +126,7 @@ <div class="info-body" v-if="!infoCollapsed"> <div class="item"> - <div class="label">Type</div> + <div class="label">Source</div> <div class="value"> <i :class="procedureTypeIconClass" /> {{ value.procedure_type }} @@ -323,7 +323,10 @@ export default { return 'fab fa-python' if (this.value.procedure_type === 'config') - return 'fas fa-rectangle-list' + return 'fas fa-file' + + if (this.value.procedure_type === 'db') + return 'fas fa-database' return this.defaultIconClass }, diff --git a/platypush/backend/http/webapp/src/utils/Text.vue b/platypush/backend/http/webapp/src/utils/Text.vue index 67dfd9d01..c87ebde28 100644 --- a/platypush/backend/http/webapp/src/utils/Text.vue +++ b/platypush/backend/http/webapp/src/utils/Text.vue @@ -37,6 +37,16 @@ export default { formatNumber(number) { return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") }, + + escapeHTML(value) { + return value + ?.toString?.() + ?.replace?.(/&/g, "&") + ?.replace?.(/</g, "<") + ?.replace?.(/>/g, ">") + ?.replace?.(/"/g, """) + ?.replace?.(/'/g, "'") || '' + }, }, } </script>