Fixes and refactoring for deleteBlock functionality. moveBlock added, drag and drop connected. UX updates for the test data section. Fixes for layout in automation step UI
This commit is contained in:
parent
071a5b1d46
commit
cf7e00eb33
|
@ -0,0 +1,451 @@
|
|||
<script>
|
||||
import { onDestroy, tick } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
import { setContext, onMount } from "svelte"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
import { selectedAutomation, automationStore } from "stores/builder"
|
||||
|
||||
export function zoomIn() {
|
||||
view.update(state => ({
|
||||
...state,
|
||||
scale: Math.min(state.scale + 0.1, 1),
|
||||
}))
|
||||
}
|
||||
|
||||
export function zoomOut() {
|
||||
view.update(state => ({
|
||||
...state,
|
||||
scale: Math.max(state.scale - 0.1, 0),
|
||||
}))
|
||||
}
|
||||
|
||||
export async function reset() {
|
||||
contentDims = {
|
||||
...contentDims,
|
||||
w: contentDims.original.w,
|
||||
h: contentDims.original.h,
|
||||
}
|
||||
dragOffset = []
|
||||
contentPos.update(state => ({
|
||||
...state,
|
||||
x: 0,
|
||||
y: 0,
|
||||
}))
|
||||
view.update(state => ({
|
||||
...state,
|
||||
scale: 1,
|
||||
}))
|
||||
}
|
||||
|
||||
export async function zoomToFit() {
|
||||
const { width: wViewPort, height: hViewPort } =
|
||||
viewPort.getBoundingClientRect()
|
||||
|
||||
const scaleTarget = Math.min(
|
||||
wViewPort / contentDims.original.w,
|
||||
hViewPort / contentDims.original.h
|
||||
)
|
||||
|
||||
// Smallest ratio determines which dimension needs squeezed
|
||||
view.update(state => ({
|
||||
...state,
|
||||
scale: scaleTarget,
|
||||
}))
|
||||
|
||||
await tick()
|
||||
|
||||
const adjustedY = (hViewPort - contentDims.original.h) / 2
|
||||
|
||||
contentPos.update(state => ({
|
||||
...state,
|
||||
x: 0,
|
||||
y: parseInt(0 + adjustedY),
|
||||
}))
|
||||
}
|
||||
|
||||
// View State
|
||||
const view = writable({
|
||||
dragging: false,
|
||||
moveStep: null,
|
||||
dragSpot: null,
|
||||
dragging: false,
|
||||
scale: 1,
|
||||
dropzones: {},
|
||||
//focus - node to center on?
|
||||
})
|
||||
|
||||
setContext("view", view)
|
||||
|
||||
// View internal pos tracking
|
||||
const internalPos = writable({ x: 0, y: 0 })
|
||||
setContext("viewPos", internalPos)
|
||||
|
||||
// Content pos tracking
|
||||
const contentPos = writable({ x: 0, y: 0 })
|
||||
setContext("contentPos", contentPos)
|
||||
|
||||
// Elements
|
||||
let mainContent
|
||||
let viewPort
|
||||
let contentWrap
|
||||
|
||||
// Mouse down
|
||||
let down = false
|
||||
|
||||
// Monitor the size of the viewPort
|
||||
let observer
|
||||
|
||||
// Size of the core display content
|
||||
let contentDims = {}
|
||||
|
||||
// Size of the view port
|
||||
let viewDims = {}
|
||||
|
||||
// When dragging the content, maintain the drag start offset
|
||||
let dragOffset
|
||||
|
||||
// Auto scroll
|
||||
let scrollInterval
|
||||
|
||||
const onScale = async () => {
|
||||
await getDims()
|
||||
}
|
||||
|
||||
const getDims = async () => {
|
||||
if (!mainContent) return
|
||||
|
||||
if (!contentDims.original) {
|
||||
contentDims.original = {
|
||||
w: parseInt(mainContent.getBoundingClientRect().width),
|
||||
h: parseInt(mainContent.getBoundingClientRect().height),
|
||||
}
|
||||
}
|
||||
|
||||
viewDims = viewPort.getBoundingClientRect()
|
||||
|
||||
contentDims = {
|
||||
...contentDims,
|
||||
w: contentDims.original.w * $view.scale,
|
||||
h: contentDims.original.h * $view.scale,
|
||||
}
|
||||
}
|
||||
|
||||
const eleXY = (coords, ele) => {
|
||||
const { clientX, clientY } = coords
|
||||
const rect = ele.getBoundingClientRect()
|
||||
const x = Math.round(clientX - rect.left)
|
||||
const y = Math.round(clientY - rect.top)
|
||||
return { x: Math.max(x, 0), y: Math.max(y, 0) }
|
||||
}
|
||||
|
||||
const buildWrapStyles = (pos, scale, dims) => {
|
||||
const { x, y } = pos
|
||||
const { w, h } = dims
|
||||
return `--posX: ${x}px; --posY: ${y}px; --scale: ${scale}; --wrapH: ${h}px; --wrapW: ${w}px`
|
||||
}
|
||||
|
||||
const onViewScroll = e => {
|
||||
e.preventDefault()
|
||||
let currentScale = $view.scale
|
||||
let scrollIncrement = 35
|
||||
let xBump = 0
|
||||
let yBump = 0
|
||||
|
||||
if (e.shiftKey) {
|
||||
// Scroll horizontal - Needs Limits
|
||||
xBump = scrollIncrement * (e.deltaX < 0 ? -1 : 1)
|
||||
contentPos.update(state => ({
|
||||
...state,
|
||||
x: state.x - xBump,
|
||||
y: state.y,
|
||||
}))
|
||||
} else if (e.ctrlKey || e.metaKey) {
|
||||
// Scale
|
||||
let updatedScale
|
||||
if (e.deltaY < 0) {
|
||||
updatedScale = Math.min(1, currentScale + 0.05)
|
||||
} else if (e.deltaY > 0) {
|
||||
updatedScale = Math.max(0, currentScale - 0.05)
|
||||
}
|
||||
|
||||
view.update(state => ({
|
||||
...state,
|
||||
scale: updatedScale,
|
||||
}))
|
||||
} else {
|
||||
yBump = scrollIncrement * (e.deltaY < 0 ? -1 : 1)
|
||||
contentPos.update(state => ({
|
||||
...state,
|
||||
x: state.x,
|
||||
y: state.y - yBump,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Optimization options
|
||||
const onViewMouseMove = async e => {
|
||||
const { x, y } = eleXY(e, viewPort)
|
||||
|
||||
internalPos.update(() => ({
|
||||
x,
|
||||
y,
|
||||
}))
|
||||
|
||||
if (down && !$view.dragging) {
|
||||
contentPos.update(state => ({
|
||||
...state,
|
||||
x: x - dragOffset[0],
|
||||
y: y - dragOffset[1],
|
||||
}))
|
||||
}
|
||||
|
||||
// Needs to handle when the mouse leaves the screen
|
||||
// Needs to know the direction of movement and accelerate/decelerate
|
||||
if (y < (viewDims.height / 100) * 20 && $view.dragging) {
|
||||
if (!scrollInterval) {
|
||||
// scrollInterval = setInterval(() => {
|
||||
// contentPos = { x: contentPos.x, y: contentPos.y + 35 }
|
||||
// }, 100)
|
||||
}
|
||||
} else {
|
||||
if (scrollInterval) {
|
||||
clearInterval(scrollInterval)
|
||||
scrollInterval = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onViewDragEnd = e => {
|
||||
down = false
|
||||
dragOffset = []
|
||||
}
|
||||
|
||||
const handleDragDrop = () => {
|
||||
const sourceBlock = $selectedAutomation.blockRefs[$view.moveStep.id]
|
||||
const sourcePath = sourceBlock.pathTo
|
||||
|
||||
const dropZone = $view.dropzones[$view.droptarget]
|
||||
const destPath = dropZone?.path
|
||||
|
||||
automationStore.actions.moveBlock(
|
||||
sourcePath,
|
||||
destPath,
|
||||
$selectedAutomation.data
|
||||
)
|
||||
}
|
||||
|
||||
const onMouseUp = () => {
|
||||
if ($view.droptarget) {
|
||||
handleDragDrop()
|
||||
}
|
||||
|
||||
view.update(state => ({
|
||||
...state,
|
||||
dragging: false,
|
||||
moveStep: null,
|
||||
dragSpot: null,
|
||||
dropzones: {},
|
||||
droptarget: null,
|
||||
}))
|
||||
}
|
||||
|
||||
const onMouseMove = async e => {
|
||||
// Update viewDims to get the latest viewport dimensions
|
||||
viewDims = viewPort.getBoundingClientRect()
|
||||
|
||||
if ($view.moveStep && $view.dragging === false) {
|
||||
view.update(state => ({
|
||||
...state,
|
||||
dragging: true,
|
||||
}))
|
||||
}
|
||||
|
||||
const checkIntersection = (pos, dzRect) => {
|
||||
return (
|
||||
pos.x < dzRect.right &&
|
||||
pos.x > dzRect.left &&
|
||||
pos.y < dzRect.bottom &&
|
||||
pos.y > dzRect.top
|
||||
)
|
||||
}
|
||||
|
||||
if ($view.dragging) {
|
||||
// TODO - Need to adjust content pos
|
||||
const adjustedX =
|
||||
(e.clientX - viewDims.left - $view.moveStep.offsetX) / $view.scale
|
||||
const adjustedY =
|
||||
(e.clientY - viewDims.top - $view.moveStep.offsetY) / $view.scale
|
||||
|
||||
view.update(state => ({
|
||||
...state,
|
||||
dragSpot: {
|
||||
x: adjustedX,
|
||||
y: adjustedY,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
if ($view.moveStep && $view.dragging) {
|
||||
let hovering = false
|
||||
Object.entries($view.dropzones).forEach(entry => {
|
||||
const [dzKey, dz] = entry
|
||||
if (checkIntersection({ x: e.clientX, y: e.clientY }, dz.dims)) {
|
||||
hovering = true
|
||||
view.update(state => ({
|
||||
...state,
|
||||
droptarget: dzKey,
|
||||
}))
|
||||
}
|
||||
})
|
||||
// Ensure that when it stops hovering, it clears the drop target
|
||||
if (!hovering) {
|
||||
view.update(state => ({
|
||||
...state,
|
||||
droptarget: null,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onMoveContent = e => {
|
||||
if (down) {
|
||||
return
|
||||
}
|
||||
const { x, y } = eleXY(e, viewPort)
|
||||
|
||||
dragOffset = [Math.abs(x - $contentPos.x), Math.abs(y - $contentPos.y)]
|
||||
}
|
||||
|
||||
// Update dims after scaling
|
||||
$: {
|
||||
$view.scale
|
||||
onScale()
|
||||
}
|
||||
|
||||
// Content mouse pos and scale to css variables.
|
||||
// The wrap is set to match the content size
|
||||
$: wrapStyles = buildWrapStyles($contentPos, $view.scale, contentDims)
|
||||
|
||||
onMount(() => {
|
||||
observer = new ResizeObserver(entries => {
|
||||
if (!entries?.[0]) {
|
||||
return
|
||||
}
|
||||
getDims()
|
||||
})
|
||||
observer.observe(viewPort)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<div
|
||||
class="draggable-canvas"
|
||||
role="region"
|
||||
aria-label="Viewport for building automations"
|
||||
on:mouseup={onMouseUp}
|
||||
on:mousemove={Utils.domDebounce(onMouseMove)}
|
||||
>
|
||||
<div
|
||||
class="view"
|
||||
bind:this={viewPort}
|
||||
on:wheel={Utils.domDebounce(onViewScroll)}
|
||||
on:mousemove={Utils.domDebounce(onViewMouseMove)}
|
||||
on:mouseup={onViewDragEnd}
|
||||
on:mouseleave={onViewDragEnd}
|
||||
>
|
||||
<div class="actions">
|
||||
<!-- debug -->
|
||||
<!-- <span>
|
||||
View Pos [{viewPos.x}, {viewPos.y}]
|
||||
</span>
|
||||
<span>Down {down}</span>
|
||||
<span>Drag {$view.dragging}</span>
|
||||
<span>DragOffset {JSON.stringify(dragOffset)}</span>
|
||||
<span>Dragging {$view?.moveStep?.id || " [no]"}</span>
|
||||
<span>Scale {$view.scale}</span>
|
||||
<span>Content {JSON.stringify(contentPos)}</span> -->
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="content-wrap"
|
||||
style={wrapStyles}
|
||||
bind:this={contentWrap}
|
||||
class:dragging={down}
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
bind:this={mainContent}
|
||||
on:mousemove={Utils.domDebounce(onMoveContent)}
|
||||
on:mousedown={e => {
|
||||
if (e.which === 1 || e.button === 0) {
|
||||
down = true
|
||||
}
|
||||
}}
|
||||
on:mouseup={e => {
|
||||
if (e.which === 1 || e.button === 0) {
|
||||
down = false
|
||||
}
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
down = false
|
||||
view.update(state => ({
|
||||
...state,
|
||||
dragging: false,
|
||||
moveStep: null,
|
||||
dragSpot: null,
|
||||
dragging: false,
|
||||
dropzones: {},
|
||||
}))
|
||||
}}
|
||||
>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
position: fixed;
|
||||
padding: 8px;
|
||||
z-index: 2;
|
||||
}
|
||||
.draggable-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.content-wrap {
|
||||
min-width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: var(--wrapW);
|
||||
height: var(--wrapH);
|
||||
cursor: grab;
|
||||
}
|
||||
.content {
|
||||
/* transition: all 0.1s ease-out; */
|
||||
transform: translate(var(--posX), var(--posY)) scale(var(--scale));
|
||||
user-select: none;
|
||||
padding: 200px;
|
||||
padding-top: 200px;
|
||||
}
|
||||
|
||||
.content-wrap.dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
</style>
|
|
@ -19,7 +19,8 @@
|
|||
import { automationStore, selectedAutomation } from "stores/builder"
|
||||
import { QueryUtils } from "@budibase/frontend-core"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { createEventDispatcher, getContext } from "svelte"
|
||||
import DragZone from "./DragZone.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -30,6 +31,8 @@
|
|||
export let bindings
|
||||
export let automation
|
||||
|
||||
const view = getContext("view")
|
||||
|
||||
let drawer
|
||||
let condition
|
||||
let open = true
|
||||
|
@ -48,7 +51,7 @@
|
|||
})
|
||||
$: branchBlockRef = {
|
||||
branchNode: true,
|
||||
pathTo: (pathTo || []).concat({ branchIdx }),
|
||||
pathTo: (pathTo || []).concat({ branchIdx, branchStepId: step.id }),
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -170,7 +173,11 @@
|
|||
|
||||
<div class="separator" />
|
||||
|
||||
<FlowItemActions block={branchBlockRef} />
|
||||
{#if $view.dragging}
|
||||
<DragZone path={branchBlockRef.pathTo} />
|
||||
{:else}
|
||||
<FlowItemActions block={branchBlockRef} />
|
||||
{/if}
|
||||
|
||||
{#if step.inputs.children[branch.id]?.length}
|
||||
<div class="separator" />
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
<script>
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { generate } from "shortid"
|
||||
|
||||
export let path
|
||||
|
||||
let dropEle
|
||||
let dzid = generate()
|
||||
|
||||
const view = getContext("view")
|
||||
|
||||
onMount(() => {
|
||||
// Always return up-to-date values
|
||||
view.update(state => {
|
||||
return {
|
||||
...state,
|
||||
dropzones: {
|
||||
...(state.dropzones || {}),
|
||||
[dzid]: {
|
||||
get dims() {
|
||||
return dropEle.getBoundingClientRect()
|
||||
},
|
||||
path,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
id={`dz-${dzid}`}
|
||||
bind:this={dropEle}
|
||||
class="drag-zone"
|
||||
class:drag-over={$view?.droptarget === dzid}
|
||||
>
|
||||
<span class="move-to">Move to</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.drag-zone.drag-over {
|
||||
background-color: #1ca872b8;
|
||||
}
|
||||
.drag-zone {
|
||||
min-height: calc(var(--spectrum-global-dimension-size-225) + 12px);
|
||||
min-width: 100%;
|
||||
background-color: rgba(28, 168, 114, 0.2);
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #1ca872;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
.move-to {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -50%;
|
||||
margin: auto;
|
||||
width: fit-content;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
padding: 4px 8px;
|
||||
background-color: rgb(28, 168, 114);
|
||||
color: var(--spectrum-global-color-gray-50);
|
||||
}
|
||||
</style>
|
|
@ -6,13 +6,20 @@
|
|||
} from "stores/builder"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import TestDataModal from "./TestDataModal.svelte"
|
||||
import { Icon, notifications, Modal, Toggle } from "@budibase/bbui"
|
||||
import {
|
||||
Icon,
|
||||
notifications,
|
||||
Modal,
|
||||
Toggle,
|
||||
Button,
|
||||
ActionButton,
|
||||
} from "@budibase/bbui"
|
||||
import { ActionStepID } from "constants/backend/automations"
|
||||
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
|
||||
import StepNode from "./StepNode.svelte"
|
||||
import { memo } from "@budibase/frontend-core"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { onMount } from "svelte"
|
||||
import DraggableCanvas from "../DraggableCanvas.svelte"
|
||||
|
||||
export let automation
|
||||
|
||||
|
@ -23,6 +30,7 @@
|
|||
let scrolling = false
|
||||
let blockRefs = {}
|
||||
let treeEle
|
||||
let draggable
|
||||
|
||||
// Memo auto - selectedAutomation
|
||||
$: memoAutomation.set(automation)
|
||||
|
@ -67,44 +75,37 @@
|
|||
notifications.error("Error deleting automation")
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = e => {
|
||||
if (e.target.scrollTop >= 30) {
|
||||
scrolling = true
|
||||
} else if (e.target.scrollTop) {
|
||||
// Set scrolling back to false if scrolled back to less than 100px
|
||||
scrolling = false
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Ensure the trigger element is centered in the view on load.
|
||||
const triggerBlock = treeEle?.querySelector(".block.TRIGGER")
|
||||
triggerBlock?.scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "nearest",
|
||||
inline: "center",
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="header" class:scrolling>
|
||||
<div class="header-left">
|
||||
<UndoRedoControl store={automationHistoryStore} />
|
||||
<UndoRedoControl store={automationHistoryStore} showButtonGroup />
|
||||
<div class="group">
|
||||
<ActionButton icon="Add" quiet on:click={draggable.zoomIn} />
|
||||
<ActionButton icon="Remove" quiet on:click={draggable.zoomOut} />
|
||||
</div>
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
draggable.zoomToFit()
|
||||
}}
|
||||
>
|
||||
Zoom to fit
|
||||
</Button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div
|
||||
class:disabled={!automation?.definition?.trigger}
|
||||
<Button
|
||||
icon={"Play"}
|
||||
cta
|
||||
disabled={!automation?.definition?.trigger}
|
||||
on:click={() => {
|
||||
testDataModal.show()
|
||||
}}
|
||||
class="buttons"
|
||||
>
|
||||
<Icon size="M" name="Play" />
|
||||
<div>Run test</div>
|
||||
</div>
|
||||
Run test
|
||||
</Button>
|
||||
<div class="buttons">
|
||||
<Icon disabled={!$automationStore.testResults} size="M" name="Multiple" />
|
||||
<div
|
||||
|
@ -117,7 +118,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{#if !isRowAction}
|
||||
<div class="setting-spacing">
|
||||
<div class="toggle-active setting-spacing">
|
||||
<Toggle
|
||||
text={automation.disabled ? "Paused" : "Activated"}
|
||||
on:change={automationStore.actions.toggleDisabled(
|
||||
|
@ -131,25 +132,25 @@
|
|||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="canvas" on:scroll={handleScroll}>
|
||||
<div class="content">
|
||||
<div class="tree">
|
||||
<div class="root" bind:this={treeEle}>
|
||||
{#if Object.keys(blockRefs).length}
|
||||
{#each blocks as block, idx (block.id)}
|
||||
<StepNode
|
||||
step={blocks[idx]}
|
||||
stepIdx={idx}
|
||||
isLast={blocks?.length - 1 === idx}
|
||||
automation={$memoAutomation}
|
||||
blocks={blockRefs}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="root" bind:this={treeEle}>
|
||||
<DraggableCanvas bind:this={draggable}>
|
||||
<span class="main-content" slot="content">
|
||||
{#if Object.keys(blockRefs).length}
|
||||
{#each blocks as block, idx (block.id)}
|
||||
<StepNode
|
||||
step={blocks[idx]}
|
||||
stepIdx={idx}
|
||||
isLast={blocks?.length - 1 === idx}
|
||||
automation={$memoAutomation}
|
||||
blocks={blockRefs}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</span>
|
||||
</DraggableCanvas>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
okText="Delete Automation"
|
||||
|
@ -166,24 +167,31 @@
|
|||
</Modal>
|
||||
|
||||
<style>
|
||||
.canvas {
|
||||
padding: var(--spacing-l) var(--spacing-xl);
|
||||
overflow-y: auto;
|
||||
.toggle-active :global(.spectrum-Switch) {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.root :global(.main-content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.header-left :global(div) {
|
||||
border-right: none;
|
||||
}
|
||||
/* Fix for firefox not respecting bottom padding in scrolling containers */
|
||||
.canvas > *:last-child {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.root {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.root :global(.block) {
|
||||
|
@ -198,22 +206,12 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 23px 23px 80px;
|
||||
box-sizing: border-box;
|
||||
/* overflow-x: hidden; */
|
||||
}
|
||||
|
||||
.header.scrolling {
|
||||
background: var(--background);
|
||||
border-bottom: var(--border-light);
|
||||
z-index: 1;
|
||||
}
|
||||
.tree {
|
||||
justify-content: center;
|
||||
display: inline-flex;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
|
@ -221,7 +219,7 @@
|
|||
align-items: center;
|
||||
padding-left: var(--spacing-l);
|
||||
transition: background 130ms ease-out;
|
||||
flex: 0 0 48px;
|
||||
flex: 0 0 60px;
|
||||
padding-right: var(--spacing-xl);
|
||||
}
|
||||
|
||||
|
@ -245,4 +243,33 @@
|
|||
pointer-events: none;
|
||||
color: var(--spectrum-global-color-gray-500) !important;
|
||||
}
|
||||
|
||||
.group {
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.group :global(> *:not(:first-child)) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: 2px solid var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.group :global(> *:not(:last-child)) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.header-left .group :global(.spectrum-Button),
|
||||
.header-left .group :global(.spectrum-ActionButton),
|
||||
.header-left .group :global(.spectrum-Icon) {
|
||||
color: var(--spectrum-global-color-gray-900) !important;
|
||||
}
|
||||
.header-left .group :global(.spectrum-Button),
|
||||
.header-left .group :global(.spectrum-ActionButton) {
|
||||
background: var(--spectrum-global-color-gray-200) !important;
|
||||
}
|
||||
.header-left .group :global(.spectrum-Button:hover),
|
||||
.header-left .group :global(.spectrum-ActionButton:hover) {
|
||||
background: var(--spectrum-global-color-gray-300) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
Label,
|
||||
AbsTooltip,
|
||||
InlineAlert,
|
||||
Helpers,
|
||||
} from "@budibase/bbui"
|
||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||
|
@ -22,6 +23,9 @@
|
|||
import { ActionStepID, TriggerStepID } from "constants/backend/automations"
|
||||
import { AutomationStepType } from "@budibase/types"
|
||||
import FlowItemActions from "./FlowItemActions.svelte"
|
||||
import DragHandle from "components/design/settings/controls/DraggableList/drag-handle.svelte"
|
||||
import { getContext } from "svelte"
|
||||
import DragZone from "./DragZone.svelte"
|
||||
|
||||
export let block
|
||||
export let blockRef
|
||||
|
@ -29,12 +33,17 @@
|
|||
export let idx
|
||||
export let automation
|
||||
export let bindings
|
||||
export let draggable = true
|
||||
|
||||
const view = getContext("view")
|
||||
const pos = getContext("viewPos")
|
||||
const contentPos = getContext("contentPos")
|
||||
|
||||
let selected
|
||||
let webhookModal
|
||||
let open = true
|
||||
let showLooping = false
|
||||
let role
|
||||
let blockEle
|
||||
|
||||
$: pathSteps = loadSteps(blockRef)
|
||||
|
||||
|
@ -55,9 +64,36 @@
|
|||
$: isAppAction = block?.stepId === TriggerStepID.APP
|
||||
$: isAppAction && setPermissions(role)
|
||||
$: isAppAction && getPermissions(automationId)
|
||||
|
||||
$: triggerInfo = $selectedAutomationDisplayData?.triggerInfo
|
||||
|
||||
$: selected = $view?.moveStep && $view?.moveStep?.id === block.id
|
||||
$: blockDims = blockEle?.getBoundingClientRect()
|
||||
$: placeholderDims = buildPlaceholderStyles(blockDims)
|
||||
|
||||
let positionStyles
|
||||
|
||||
// Move the selected item
|
||||
$: move(blockEle, $view?.dragSpot, selected)
|
||||
|
||||
const move = (block, dragPos, selected) => {
|
||||
if ((!block && !selected) || !dragPos) {
|
||||
return
|
||||
}
|
||||
positionStyles = `
|
||||
--blockPosX: ${Math.round(dragPos.x)}px;
|
||||
--blockPosY: ${Math.round(dragPos.y)}px;
|
||||
`
|
||||
}
|
||||
|
||||
const buildPlaceholderStyles = dims => {
|
||||
if (!dims) {
|
||||
return ""
|
||||
}
|
||||
const { width, height } = dims
|
||||
return `--pswidth: ${Math.round(width)}px;
|
||||
--psheight: ${Math.round(height)}px;`
|
||||
}
|
||||
|
||||
async function setPermissions(role) {
|
||||
if (!role || !automationId) {
|
||||
return
|
||||
|
@ -103,6 +139,24 @@
|
|||
blockRef.pathTo
|
||||
)
|
||||
}
|
||||
|
||||
const onHandleMouseDown = e => {
|
||||
if (isTrigger) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
e.stopPropagation()
|
||||
|
||||
view.update(state => ({
|
||||
...state,
|
||||
moveStep: {
|
||||
id: block.id,
|
||||
offsetX: $pos.x,
|
||||
offsetY: $pos.y,
|
||||
},
|
||||
}))
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if block.stepId !== "LOOP"}
|
||||
|
@ -112,124 +166,159 @@
|
|||
id={`block-${block.id}`}
|
||||
class={`block ${block.type} hoverable`}
|
||||
class:selected
|
||||
on:click={() => {}}
|
||||
class:draggable
|
||||
>
|
||||
{#if loopBlock}
|
||||
<div class="blockSection">
|
||||
<div
|
||||
on:click={() => {
|
||||
showLooping = !showLooping
|
||||
}}
|
||||
class="splitHeader"
|
||||
>
|
||||
<div class="center-items">
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
class="spectrum-Icon"
|
||||
style="color:var(--spectrum-global-color-gray-700);"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-Reuse" />
|
||||
</svg>
|
||||
<div class="iconAlign">
|
||||
<Detail size="S">Looping</Detail>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="blockTitle">
|
||||
<AbsTooltip type="negative" text="Remove looping">
|
||||
<Icon on:click={removeLooping} hoverable name="DeleteOutline" />
|
||||
</AbsTooltip>
|
||||
|
||||
<div style="margin-left: 10px;" on:click={() => {}}>
|
||||
<Icon
|
||||
hoverable
|
||||
name={showLooping ? "ChevronDown" : "ChevronUp"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider noMargin />
|
||||
{#if !showLooping}
|
||||
<div class="blockSection">
|
||||
<Layout noPadding gap="S">
|
||||
<AutomationBlockSetup
|
||||
schemaProperties={Object.entries(
|
||||
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
|
||||
.properties
|
||||
)}
|
||||
{webhookModal}
|
||||
block={loopBlock}
|
||||
{automation}
|
||||
{bindings}
|
||||
/>
|
||||
</Layout>
|
||||
</div>
|
||||
<Divider noMargin />
|
||||
<div class="wrap">
|
||||
{#if $view.dragging && selected}
|
||||
<div class="drag-placeholder" style={placeholderDims} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<FlowItemHeader
|
||||
{automation}
|
||||
{open}
|
||||
{block}
|
||||
{testDataModal}
|
||||
{idx}
|
||||
{addLooping}
|
||||
{deleteStep}
|
||||
on:toggle={() => (open = !open)}
|
||||
on:update={async e => {
|
||||
const newName = e.detail
|
||||
if (newName.length === 0) {
|
||||
await automationStore.actions.deleteAutomationName(block.id)
|
||||
} else {
|
||||
await automationStore.actions.saveAutomationName(block.id, newName)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if open}
|
||||
<Divider noMargin />
|
||||
<div class="blockSection">
|
||||
<Layout noPadding gap="S">
|
||||
{#if isAppAction}
|
||||
<div>
|
||||
<Label>Role</Label>
|
||||
<RoleSelect bind:value={role} />
|
||||
<div
|
||||
bind:this={blockEle}
|
||||
class="block-content"
|
||||
class:dragging={$view.dragging && selected}
|
||||
style={positionStyles}
|
||||
>
|
||||
{#if draggable}
|
||||
<div
|
||||
class="handle"
|
||||
class:grabbing={selected}
|
||||
on:mousedown={onHandleMouseDown}
|
||||
>
|
||||
<DragHandle />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="block-core">
|
||||
{#if loopBlock}
|
||||
<div class="blockSection">
|
||||
<div
|
||||
on:click={() => {
|
||||
showLooping = !showLooping
|
||||
}}
|
||||
class="splitHeader"
|
||||
>
|
||||
<div class="center-items">
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
class="spectrum-Icon"
|
||||
style="color:var(--spectrum-global-color-gray-700);"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-Reuse" />
|
||||
</svg>
|
||||
<div class="iconAlign">
|
||||
<Detail size="S">Looping</Detail>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="blockTitle">
|
||||
<AbsTooltip type="negative" text="Remove looping">
|
||||
<Icon
|
||||
on:click={removeLooping}
|
||||
hoverable
|
||||
name="DeleteOutline"
|
||||
/>
|
||||
</AbsTooltip>
|
||||
|
||||
<div style="margin-left: 10px;" on:click={() => {}}>
|
||||
<Icon
|
||||
hoverable
|
||||
name={showLooping ? "ChevronDown" : "ChevronUp"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider noMargin />
|
||||
{#if !showLooping}
|
||||
<div class="blockSection">
|
||||
<Layout noPadding gap="S">
|
||||
<AutomationBlockSetup
|
||||
schemaProperties={Object.entries(
|
||||
$automationStore.blockDefinitions.ACTION.LOOP.schema
|
||||
.inputs.properties
|
||||
)}
|
||||
{webhookModal}
|
||||
block={loopBlock}
|
||||
{automation}
|
||||
{bindings}
|
||||
/>
|
||||
</Layout>
|
||||
</div>
|
||||
<Divider noMargin />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<FlowItemHeader
|
||||
{automation}
|
||||
{open}
|
||||
{block}
|
||||
{testDataModal}
|
||||
{idx}
|
||||
{addLooping}
|
||||
{deleteStep}
|
||||
on:toggle={() => (open = !open)}
|
||||
on:update={async e => {
|
||||
const newName = e.detail
|
||||
if (newName.length === 0) {
|
||||
await automationStore.actions.deleteAutomationName(block.id)
|
||||
} else {
|
||||
await automationStore.actions.saveAutomationName(
|
||||
block.id,
|
||||
newName
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if open}
|
||||
<Divider noMargin />
|
||||
<div class="blockSection">
|
||||
<Layout noPadding gap="S">
|
||||
{#if isAppAction}
|
||||
<div>
|
||||
<Label>Role</Label>
|
||||
<RoleSelect bind:value={role} />
|
||||
</div>
|
||||
{/if}
|
||||
<AutomationBlockSetup
|
||||
schemaProperties={Object.entries(
|
||||
block?.schema?.inputs?.properties || {}
|
||||
)}
|
||||
{block}
|
||||
{webhookModal}
|
||||
{automation}
|
||||
{bindings}
|
||||
/>
|
||||
{#if isTrigger && triggerInfo}
|
||||
<InlineAlert
|
||||
header={triggerInfo.type}
|
||||
message={`This trigger is tied to the "${triggerInfo.rowAction.name}" row action in your ${triggerInfo.table.name} table`}
|
||||
/>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
{/if}
|
||||
<AutomationBlockSetup
|
||||
schemaProperties={Object.entries(
|
||||
block?.schema?.inputs?.properties || {}
|
||||
)}
|
||||
{block}
|
||||
{webhookModal}
|
||||
{automation}
|
||||
{bindings}
|
||||
/>
|
||||
{#if isTrigger && triggerInfo}
|
||||
<InlineAlert
|
||||
header={triggerInfo.type}
|
||||
message={`This trigger is tied to the "${triggerInfo.rowAction.name}" row action in your ${triggerInfo.table.name} table`}
|
||||
/>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if !collectBlockExists || !lastStep}
|
||||
<div class="separator" />
|
||||
<FlowItemActions
|
||||
{block}
|
||||
on:branch={() => {
|
||||
automationStore.actions.branchAutomation(
|
||||
$selectedAutomation.blockRefs[block.id].pathTo,
|
||||
automation
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{#if $view.dragging}
|
||||
<DragZone path={blockRef?.pathTo} />
|
||||
{:else}
|
||||
<FlowItemActions
|
||||
{block}
|
||||
on:branch={() => {
|
||||
automationStore.actions.branchAutomation(
|
||||
$selectedAutomation.blockRefs[block.id].pathTo,
|
||||
automation
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if !lastStep}
|
||||
<div class="separator" />
|
||||
{/if}
|
||||
|
@ -266,27 +355,73 @@
|
|||
.block {
|
||||
width: 480px;
|
||||
font-size: 16px;
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 4px 4px 4px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.block .wrap {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.block.draggable .wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.block.draggable .wrap .handle {
|
||||
height: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--grey-3);
|
||||
padding: 6px;
|
||||
color: var(--grey-6);
|
||||
cursor: grab;
|
||||
}
|
||||
.block.draggable .wrap .handle.grabbing {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.block.draggable .wrap .handle :global(.drag-handle) {
|
||||
width: 6px;
|
||||
}
|
||||
.block .wrap .block-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--grey-3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.blockSection {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 1px;
|
||||
height: 25px;
|
||||
border-left: 1px dashed var(--grey-4);
|
||||
color: var(--grey-4);
|
||||
/* center horizontally */
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.blockTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
.drag-placeholder {
|
||||
height: calc(var(--psheight) - 2px);
|
||||
width: var(--pswidth);
|
||||
background-color: rgba(92, 92, 92, 0.1);
|
||||
border: 1px dashed #5c5c5c;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
}
|
||||
.block-core {
|
||||
flex: 1;
|
||||
}
|
||||
.block-core.dragging {
|
||||
pointer-events: none;
|
||||
}
|
||||
.block-content.dragging {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: var(--blockPosY);
|
||||
left: var(--blockPosX);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
tooltipType={TooltipType.Info}
|
||||
tooltipPosition={TooltipPosition.Left}
|
||||
tooltip={"Create branch"}
|
||||
size={"S"}
|
||||
/>
|
||||
{/if}
|
||||
<Icon
|
||||
|
@ -35,6 +36,7 @@
|
|||
tooltipType={TooltipType.Info}
|
||||
tooltipPosition={TooltipPosition.Right}
|
||||
tooltip={"Add a step"}
|
||||
size={"S"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -44,6 +46,6 @@
|
|||
border-radius: 4px 4px 4px 4px;
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
padding: var(--spacing-m);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
automationStore.actions.branchAutomation(pathToCurrentNode, automation)
|
||||
}}
|
||||
>
|
||||
Add additional branch
|
||||
Add branch
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div class="branched">
|
||||
|
@ -105,7 +105,6 @@
|
|||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<!--Drop Zone-->
|
||||
<div class="block">
|
||||
<FlowItem
|
||||
block={step}
|
||||
|
@ -114,9 +113,9 @@
|
|||
{isLast}
|
||||
{automation}
|
||||
{bindings}
|
||||
draggable={step.type !== "TRIGGER"}
|
||||
/>
|
||||
</div>
|
||||
<!--Drop Zone-->
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
export let automation
|
||||
export let testResults
|
||||
export let width = "400px"
|
||||
|
||||
let openBlocks = {}
|
||||
let blocks
|
||||
|
@ -65,7 +64,7 @@
|
|||
|
||||
<div class="container">
|
||||
{#each blocks as block, idx}
|
||||
<div class="block" style={width ? `width: ${width}` : ""}>
|
||||
<div class="block">
|
||||
{#if block.stepId !== ActionStepID.LOOP}
|
||||
<FlowItemHeader
|
||||
{automation}
|
||||
|
@ -146,7 +145,10 @@
|
|||
.container {
|
||||
padding: 0 30px 30px 30px;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
|
@ -188,17 +190,17 @@
|
|||
|
||||
.block {
|
||||
display: inline-block;
|
||||
width: 400px;
|
||||
height: auto;
|
||||
height: fit-content;
|
||||
font-size: 16px;
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 4px 4px 4px 4px;
|
||||
min-width: 425px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
flex: 0 0 40px;
|
||||
border-left: 1px dashed var(--grey-4);
|
||||
color: var(--grey-4);
|
||||
/* center horizontally */
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<div class="title">
|
||||
<div class="title-text">
|
||||
<Icon name="MultipleCheck" />
|
||||
<div style="padding-left: var(--spacing-l); ">Test Details</div>
|
||||
<div>Test Details</div>
|
||||
</div>
|
||||
<div style="padding-right: var(--spacing-xl)">
|
||||
<div>
|
||||
<Icon
|
||||
on:click={async () => {
|
||||
$automationStore.showTestPanel = false
|
||||
|
@ -32,7 +32,8 @@
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding-left: var(--spacing-xl);
|
||||
padding: 0px 30px;
|
||||
padding-top: var(--spacing-l);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
@ -40,7 +41,7 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-top: var(--spacing-s);
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.title :global(h1) {
|
||||
|
|
|
@ -690,7 +690,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class:field-width={shouldRenderField(value)}>
|
||||
<div>
|
||||
{#if value.type === "string" && value.enum && canShowField(key, value)}
|
||||
<Select
|
||||
on:change={e => onChange({ [key]: e.detail })}
|
||||
|
@ -986,6 +986,11 @@
|
|||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.label-container :global(label) {
|
||||
white-space: unset;
|
||||
}
|
||||
|
||||
.field-width {
|
||||
width: 320px;
|
||||
}
|
||||
|
@ -999,12 +1004,9 @@
|
|||
}
|
||||
|
||||
.block-field {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 320px;
|
||||
}
|
||||
|
||||
.attachment-field-width {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { Icon, ActionButton } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { isBuilderInputFocused } from "helpers"
|
||||
|
||||
export let store
|
||||
export let showButtonGroup = false
|
||||
|
||||
const handleKeyPress = e => {
|
||||
if (!(e.ctrlKey || e.metaKey)) {
|
||||
|
@ -32,19 +33,36 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<div class="undo-redo">
|
||||
<Icon
|
||||
name="Undo"
|
||||
hoverable
|
||||
on:click={store.undo}
|
||||
disabled={!$store.canUndo}
|
||||
/>
|
||||
<Icon
|
||||
name="Redo"
|
||||
hoverable
|
||||
on:click={store.redo}
|
||||
disabled={!$store.canRedo}
|
||||
/>
|
||||
<div class="undo-redo" class:buttons={showButtonGroup}>
|
||||
{#if showButtonGroup}
|
||||
<div class="group">
|
||||
<ActionButton
|
||||
icon="Undo"
|
||||
quiet
|
||||
on:click={store.undo}
|
||||
disabled={!$store.canUndo}
|
||||
/>
|
||||
<ActionButton
|
||||
icon="Redo"
|
||||
quiet
|
||||
on:click={store.redo}
|
||||
disabled={!$store.canRedo}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<Icon
|
||||
name="Undo"
|
||||
hoverable
|
||||
on:click={store.undo}
|
||||
disabled={!$store.canUndo}
|
||||
/>
|
||||
<Icon
|
||||
name="Redo"
|
||||
hoverable
|
||||
on:click={store.redo}
|
||||
disabled={!$store.canRedo}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -60,4 +78,36 @@
|
|||
.undo-redo :global(svg) {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.undo-redo.buttons :global(svg) {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.undo-redo.buttons {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.group {
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.group :global(> *:not(:first-child)) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: 2px solid var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.group :global(> *:not(:last-child)) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.group :global(.spectrum-Button),
|
||||
.group :global(.spectrum-ActionButton) {
|
||||
background: var(--spectrum-global-color-gray-200) !important;
|
||||
}
|
||||
.group :global(.spectrum-Button:hover),
|
||||
.group :global(.spectrum-ActionButton:hover) {
|
||||
background: var(--spectrum-global-color-gray-300) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -30,7 +30,6 @@ const initialAutomationState = {
|
|||
},
|
||||
selectedAutomationId: null,
|
||||
automationDisplayData: {},
|
||||
blocks: {},
|
||||
}
|
||||
|
||||
// If this functions, remove the actions elements
|
||||
|
@ -66,6 +65,147 @@ const getFinalDefinitions = (triggers, actions) => {
|
|||
}
|
||||
|
||||
const automationActions = store => ({
|
||||
/**
|
||||
* Move a given block from one location on the tree to another.
|
||||
*
|
||||
* @param {Object} sourcePath path to the block to be moved
|
||||
* @param {Object} destPath the destinationPart
|
||||
* @param {Object} automation the automaton to be mutated
|
||||
*/
|
||||
moveBlock: async (sourcePath, destPath, automation) => {
|
||||
// Use core delete to remove and return the deleted block
|
||||
// from the automation
|
||||
const { deleted, newAutomation } = store.actions.deleteBlock(
|
||||
sourcePath,
|
||||
automation
|
||||
)
|
||||
|
||||
// Traverse again as deleting the node from its original location
|
||||
// will redefine all proceding node locations
|
||||
const newRefs = {}
|
||||
store.actions.traverse(newRefs, newAutomation)
|
||||
|
||||
// The last part of the destination node address, containing the id.
|
||||
const pathEnd = destPath.at(-1)
|
||||
|
||||
let finalPath
|
||||
// If dropping in a branch-step dropzone you need to find
|
||||
// the updated parent step route then add the branch details again
|
||||
if (pathEnd.branchStepId) {
|
||||
const branchStepRef = newRefs[pathEnd.branchStepId]
|
||||
finalPath = branchStepRef.pathTo
|
||||
finalPath.push(pathEnd)
|
||||
} else {
|
||||
// Place the target 1 after the drop
|
||||
finalPath = newRefs[pathEnd.id].pathTo
|
||||
finalPath.at(-1).stepIdx += 1
|
||||
}
|
||||
|
||||
// Uses the updated tree refs to resolve the new position
|
||||
// for the moved element.
|
||||
const updated = store.actions.updateStep(
|
||||
finalPath,
|
||||
newAutomation,
|
||||
deleted,
|
||||
true
|
||||
)
|
||||
|
||||
try {
|
||||
await store.actions.save(updated)
|
||||
} catch (e) {
|
||||
notifications.error("Error moving automation block")
|
||||
console.error("Error moving automation block ", e)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Core delete function that will delete the node at the provided
|
||||
* location. Loops require 2 deletes so the function returns an array.
|
||||
* The passed in automation is not mutated
|
||||
*
|
||||
* @param {*} pathTo the tree path to the target node
|
||||
* @param {*} automation the automation to alter.
|
||||
* @returns {Object} contains the deleted nodes and new updated automation
|
||||
*/
|
||||
deleteBlock: (pathTo, automation) => {
|
||||
let newAutomation = cloneDeep(automation)
|
||||
|
||||
const steps = [
|
||||
newAutomation.definition.trigger,
|
||||
...newAutomation.definition.steps,
|
||||
]
|
||||
|
||||
let cache
|
||||
pathTo.forEach((path, pathIdx, array) => {
|
||||
const final = pathIdx === array.length - 1
|
||||
const { stepIdx, branchIdx } = path
|
||||
|
||||
const deleteCore = (steps, idx) => {
|
||||
const targetBlock = steps[idx]
|
||||
// By default, include the id of the target block
|
||||
const idsToDelete = [targetBlock.id]
|
||||
const blocksDeleted = []
|
||||
|
||||
// If deleting a looped block, ensure all related block references are
|
||||
// collated beforehand. Delete can then be handled atomically
|
||||
const loopSteps = {}
|
||||
steps.forEach(child => {
|
||||
const { blockToLoop, id: loopBlockId } = child
|
||||
if (blockToLoop) {
|
||||
// The loop block > the block it loops
|
||||
loopSteps[blockToLoop] = loopBlockId
|
||||
}
|
||||
})
|
||||
|
||||
// Check if there is a related loop block to remove
|
||||
const loopStep = loopSteps[targetBlock.id]
|
||||
if (loopStep) {
|
||||
idsToDelete.push(loopStep)
|
||||
}
|
||||
|
||||
// Purge all ids related to the block being deleted
|
||||
for (let i = steps.length - 1; i >= 0; i--) {
|
||||
if (idsToDelete.includes(steps[i].id)) {
|
||||
const [deletedBlock] = steps.splice(i, 1)
|
||||
blocksDeleted.unshift(deletedBlock)
|
||||
}
|
||||
}
|
||||
|
||||
return { deleted: blocksDeleted, newAutomation }
|
||||
}
|
||||
|
||||
if (!cache) {
|
||||
// If the path history is empty and on the final step
|
||||
// delete the specified target
|
||||
if (final) {
|
||||
cache = deleteCore(
|
||||
newAutomation.definition.steps,
|
||||
stepIdx > 0 ? stepIdx - 1 : 0
|
||||
)
|
||||
} else {
|
||||
// Return the root node
|
||||
cache = steps[stepIdx]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (Number.isInteger(branchIdx)) {
|
||||
const branchId = cache.inputs.branches[branchIdx].id
|
||||
const children = cache.inputs.children[branchId]
|
||||
const currentBlock = children[stepIdx]
|
||||
|
||||
if (final) {
|
||||
cache = deleteCore(children, stepIdx)
|
||||
} else {
|
||||
cache = currentBlock
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// should be 1-2 blocks in an array
|
||||
return cache
|
||||
},
|
||||
|
||||
/**
|
||||
* Build metadata for the automation tree. Store the path and
|
||||
* note any loop information used when rendering
|
||||
|
@ -78,7 +218,6 @@ const automationActions = store => ({
|
|||
blocks[block.id] = {
|
||||
...(blocks[block.id] || {}),
|
||||
pathTo,
|
||||
bindings: [],
|
||||
terminating: terminating || false,
|
||||
...(block.blockToLoop ? { blockToLoop: block.blockToLoop } : {}),
|
||||
}
|
||||
|
@ -127,16 +266,26 @@ const automationActions = store => ({
|
|||
|
||||
/**
|
||||
* Take an updated step and replace it in the specified location
|
||||
* on the automation
|
||||
* on the automation. If `insert` is set to true, the supplied block/s
|
||||
* will be inserted instead.
|
||||
*
|
||||
* @param {Array<Object>} pathWay - the full path to the tree node and the step
|
||||
* @param {Object} automation - the automation to be mutated
|
||||
* @param {Object} update - the block to replace
|
||||
* @param {Array<Object>} pathWay the full path to the tree node and the step
|
||||
* @param {Object} automation the automation to be mutated
|
||||
* @param {Object} update the block to replace
|
||||
* @param {Boolean} insert defaults to false
|
||||
* @returns
|
||||
*/
|
||||
updateStep: (pathWay, automation, update) => {
|
||||
updateStep: (pathWay, automation, update, insert = false) => {
|
||||
let newAutomation = cloneDeep(automation)
|
||||
|
||||
const finalise = (dest, idx, update) => {
|
||||
dest.splice(
|
||||
idx,
|
||||
insert ? 0 : update.length || 1,
|
||||
...(Array.isArray(update) ? update : [update])
|
||||
)
|
||||
}
|
||||
|
||||
let cache = null
|
||||
pathWay.forEach((path, idx, array) => {
|
||||
const { stepIdx, branchIdx } = path
|
||||
|
@ -146,7 +295,7 @@ const automationActions = store => ({
|
|||
// Trigger offset
|
||||
let idx = Math.max(stepIdx - 1, 0)
|
||||
if (final) {
|
||||
newAutomation.definition.steps[idx] = update
|
||||
finalise(newAutomation.definition.steps, idx, update)
|
||||
return
|
||||
}
|
||||
cache = newAutomation.definition.steps[idx]
|
||||
|
@ -156,8 +305,7 @@ const automationActions = store => ({
|
|||
const branchId = cache.inputs.branches[branchIdx].id
|
||||
const children = cache.inputs.children[branchId]
|
||||
if (final) {
|
||||
// replace the node and return it
|
||||
children[stepIdx] = update
|
||||
finalise(children, stepIdx, update)
|
||||
} else {
|
||||
cache = children[stepIdx]
|
||||
}
|
||||
|
@ -171,7 +319,7 @@ const automationActions = store => ({
|
|||
* If the current license covers Environment variables,
|
||||
* all environment variables will be output as bindings
|
||||
*
|
||||
* @returns {Array<Object>} - all available environment bindings
|
||||
* @returns {Array<Object>} all available environment bindings
|
||||
*/
|
||||
buildEnvironmentBindings: () => {
|
||||
if (get(licensing).environmentVariablesEnabled) {
|
||||
|
@ -188,8 +336,11 @@ const automationActions = store => ({
|
|||
return []
|
||||
},
|
||||
/**
|
||||
* @param {string} id - the step id of the target
|
||||
* @returns {Array<Object>} - all bindings on the path to this step
|
||||
* Take the supplied step id and aggregate all bindings for every
|
||||
* step preceding it.
|
||||
*
|
||||
* @param {string} id the step id of the target
|
||||
* @returns {Array<Object>} all bindings on the path to this step
|
||||
*/
|
||||
getPathBindings: id => {
|
||||
const block = get(selectedAutomation).blockRefs[id]
|
||||
|
@ -210,6 +361,10 @@ const automationActions = store => ({
|
|||
*/
|
||||
traverse: (blockRefs, automation) => {
|
||||
let blocks = []
|
||||
if (!automation || !blockRefs) {
|
||||
console.error("Need a valid automation")
|
||||
return
|
||||
}
|
||||
if (automation.definition?.trigger) {
|
||||
blocks.push(automation.definition.trigger)
|
||||
}
|
||||
|
@ -230,7 +385,6 @@ const automationActions = store => ({
|
|||
block.inputs?.children[branch.id].forEach((bBlock, sIdx, array) => {
|
||||
const ended =
|
||||
array.length - 1 === sIdx && !bBlock.inputs?.branches?.length
|
||||
|
||||
treeTraverse(bBlock, pathToCurrentNode, sIdx, bIdx, ended)
|
||||
})
|
||||
})
|
||||
|
@ -259,7 +413,6 @@ const automationActions = store => ({
|
|||
* @param {Object} automation The complete automation
|
||||
* @returns
|
||||
*/
|
||||
|
||||
getAvailableBindings: (block, automation) => {
|
||||
if (!block || !automation?.definition) {
|
||||
return []
|
||||
|
@ -1047,84 +1200,15 @@ const automationActions = store => ({
|
|||
},
|
||||
|
||||
/**
|
||||
* Delete the block at a given path.
|
||||
* Delete the block at a given path and save.
|
||||
* Any related blocks, like loops, are purged at the same time
|
||||
*
|
||||
* @param {Array<Object>} pathTo
|
||||
* @param {Array<Object>} pathTo the path to the target node
|
||||
*/
|
||||
deleteAutomationBlock: async pathTo => {
|
||||
const automation = get(selectedAutomation).data
|
||||
let newAutomation = cloneDeep(automation)
|
||||
const automation = get(selectedAutomation)?.data
|
||||
|
||||
const steps = [
|
||||
newAutomation.definition.trigger,
|
||||
...newAutomation.definition.steps,
|
||||
]
|
||||
|
||||
let cache
|
||||
pathTo.forEach((path, pathIdx, array) => {
|
||||
const final = pathIdx === array.length - 1
|
||||
const { stepIdx, branchIdx } = path
|
||||
|
||||
const deleteBlock = (steps, idx) => {
|
||||
const targetBlock = steps[idx]
|
||||
// By default, include the id of the target block
|
||||
const idsToDelete = [targetBlock.id]
|
||||
|
||||
// If deleting a looped block, ensure all related block references are
|
||||
// collated beforehand. Delete can then be handled atomically
|
||||
const loopSteps = {}
|
||||
steps.forEach(child => {
|
||||
const { blockToLoop, id: loopBlockId } = child
|
||||
if (blockToLoop) {
|
||||
// The loop block > the block it loops
|
||||
loopSteps[blockToLoop] = loopBlockId
|
||||
}
|
||||
})
|
||||
|
||||
// Check if there is a related loop block to remove
|
||||
const loopStep = loopSteps[targetBlock.id]
|
||||
if (loopStep) {
|
||||
idsToDelete.push(loopStep)
|
||||
}
|
||||
|
||||
// Purge all ids related to the block being deleted
|
||||
for (let i = steps.length - 1; i >= 0; i--) {
|
||||
if (idsToDelete.includes(steps[i].id)) {
|
||||
steps.splice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return idsToDelete
|
||||
}
|
||||
|
||||
if (!cache) {
|
||||
// If the path history is empty and on the final step
|
||||
// delete the specified target
|
||||
if (final) {
|
||||
cache = deleteBlock(
|
||||
newAutomation.definition.steps,
|
||||
stepIdx > 0 ? stepIdx - 1 : 0
|
||||
)
|
||||
} else {
|
||||
// Return the root node
|
||||
cache = steps[stepIdx]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (Number.isInteger(branchIdx)) {
|
||||
const branchId = cache.inputs.branches[branchIdx].id
|
||||
const children = cache.inputs.children[branchId]
|
||||
const currentBlock = children[stepIdx]
|
||||
|
||||
if (final) {
|
||||
cache = deleteBlock(children, stepIdx)
|
||||
} else {
|
||||
cache = currentBlock
|
||||
}
|
||||
}
|
||||
})
|
||||
const { newAutomation } = store.actions.deleteBlock(pathTo, automation)
|
||||
|
||||
try {
|
||||
await store.actions.save(newAutomation)
|
||||
|
|
Loading…
Reference in New Issue