Merge branch 'master' into BUDI-9127/remove-feature-flag
This commit is contained in:
commit
c1a89fc554
|
@ -35,7 +35,7 @@
|
||||||
export let footer: string | undefined = undefined
|
export let footer: string | undefined = undefined
|
||||||
export let open: boolean = false
|
export let open: boolean = false
|
||||||
export let searchTerm: string | undefined = undefined
|
export let searchTerm: string | undefined = undefined
|
||||||
export let loading: boolean | undefined = undefined
|
export let loading: boolean | undefined = false
|
||||||
export let onOptionMouseenter = () => {}
|
export let onOptionMouseenter = () => {}
|
||||||
export let onOptionMouseleave = () => {}
|
export let onOptionMouseleave = () => {}
|
||||||
export let customPopoverHeight: string | undefined = undefined
|
export let customPopoverHeight: string | undefined = undefined
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
export let compare: any = undefined
|
export let compare: any = undefined
|
||||||
export let onOptionMouseenter = () => {}
|
export let onOptionMouseenter = () => {}
|
||||||
export let onOptionMouseleave = () => {}
|
export let onOptionMouseleave = () => {}
|
||||||
|
export let loading: boolean | undefined = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = (e: CustomEvent<any>) => {
|
const onChange = (e: CustomEvent<any>) => {
|
||||||
|
@ -56,6 +57,7 @@
|
||||||
<Field {helpText} {label} {labelPosition} {error} {tooltip}>
|
<Field {helpText} {label} {labelPosition} {error} {tooltip}>
|
||||||
<Select
|
<Select
|
||||||
{quiet}
|
{quiet}
|
||||||
|
{loading}
|
||||||
{disabled}
|
{disabled}
|
||||||
{readonly}
|
{readonly}
|
||||||
{value}
|
{value}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
export let color: string | undefined = undefined
|
export let color: string | undefined = undefined
|
||||||
export let hoverColor: string | undefined = undefined
|
export let hoverColor: string | undefined = undefined
|
||||||
export let tooltip: string | undefined = undefined
|
export let tooltip: string | undefined = undefined
|
||||||
export let tooltipPosition = TooltipPosition.Bottom
|
export let tooltipPosition: TooltipPosition = TooltipPosition.Bottom
|
||||||
export let tooltipType = TooltipType.Default
|
export let tooltipType = TooltipType.Default
|
||||||
export let tooltipColor: string | undefined = undefined
|
export let tooltipColor: string | undefined = undefined
|
||||||
export let tooltipWrap: boolean = true
|
export let tooltipWrap: boolean = true
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
declare module "*.png" {
|
||||||
|
const value: string
|
||||||
|
export default value
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.svg" {
|
||||||
|
const value: string
|
||||||
|
export default value
|
||||||
|
}
|
|
@ -134,9 +134,6 @@
|
||||||
// Size of the view port
|
// Size of the view port
|
||||||
let viewDims = {}
|
let viewDims = {}
|
||||||
|
|
||||||
// Edge around the draggable content
|
|
||||||
let contentDragPadding = 200
|
|
||||||
|
|
||||||
// Auto scroll
|
// Auto scroll
|
||||||
let scrollInterval
|
let scrollInterval
|
||||||
|
|
||||||
|
@ -234,7 +231,7 @@
|
||||||
: {}),
|
: {}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
offsetX = offsetX - xBump
|
offsetX -= xBump
|
||||||
} else if (e.ctrlKey || e.metaKey) {
|
} else if (e.ctrlKey || e.metaKey) {
|
||||||
// Scale the content on scrolling
|
// Scale the content on scrolling
|
||||||
let updatedScale
|
let updatedScale
|
||||||
|
@ -261,7 +258,7 @@
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
}))
|
}))
|
||||||
offsetY = offsetY - yBump
|
offsetY -= yBump
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,8 +332,8 @@
|
||||||
scrollX: state.scrollX + xInterval,
|
scrollX: state.scrollX + xInterval,
|
||||||
scrollY: state.scrollY + yInterval,
|
scrollY: state.scrollY + yInterval,
|
||||||
}))
|
}))
|
||||||
offsetX = offsetX + xInterval
|
offsetX += xInterval
|
||||||
offsetY = offsetY + yInterval
|
offsetY += yInterval
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -488,22 +485,35 @@
|
||||||
const viewToFocusEle = () => {
|
const viewToFocusEle = () => {
|
||||||
if ($focusElement) {
|
if ($focusElement) {
|
||||||
const viewWidth = viewDims.width
|
const viewWidth = viewDims.width
|
||||||
|
const viewHeight = viewDims.height
|
||||||
|
|
||||||
// The amount to shift the content in order to center the trigger on load.
|
// The amount to shift the content in order to center the trigger on load.
|
||||||
// The content is also padded with `contentDragPadding`
|
|
||||||
// The sidebar offset factors into the left positioning of the content here.
|
// The sidebar offset factors into the left positioning of the content here.
|
||||||
const targetX =
|
const targetX =
|
||||||
contentWrap.getBoundingClientRect().x -
|
contentWrap.getBoundingClientRect().x -
|
||||||
$focusElement.x +
|
$focusElement.x +
|
||||||
(viewWidth / 2 - $focusElement.width / 2)
|
(viewWidth - $focusElement.width) / 2
|
||||||
|
|
||||||
|
const targetY =
|
||||||
|
contentWrap.getBoundingClientRect().y -
|
||||||
|
$focusElement.y +
|
||||||
|
(viewHeight - $focusElement.height) / 2
|
||||||
|
|
||||||
// Update the content position state
|
// Update the content position state
|
||||||
// Shift the content up slightly to accommodate the padding
|
// Shift the content up slightly to accommodate the padding
|
||||||
contentPos.update(state => ({
|
contentPos.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
x: targetX,
|
x: targetX,
|
||||||
y: -(contentDragPadding / 2),
|
y: Number.isInteger($focusElement.targetY)
|
||||||
|
? $focusElement.targetY
|
||||||
|
: targetY,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Be sure to set the initial offset correctly
|
||||||
|
offsetX = targetX
|
||||||
|
offsetY = Number.isInteger($focusElement.targetY)
|
||||||
|
? $focusElement.targetY
|
||||||
|
: targetY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -550,7 +560,6 @@
|
||||||
aria-label="Viewport for building automations"
|
aria-label="Viewport for building automations"
|
||||||
on:mouseup={onMouseUp}
|
on:mouseup={onMouseUp}
|
||||||
on:mousemove={Utils.domDebounce(onMouseMove)}
|
on:mousemove={Utils.domDebounce(onMouseMove)}
|
||||||
style={`--dragPadding: ${contentDragPadding}px;`}
|
|
||||||
>
|
>
|
||||||
<svg class="draggable-background" style={`--dotSize: ${dotSize};`}>
|
<svg class="draggable-background" style={`--dotSize: ${dotSize};`}>
|
||||||
<!-- Small 2px offset to tuck the points under the viewport on load-->
|
<!-- Small 2px offset to tuck the points under the viewport on load-->
|
||||||
|
@ -617,7 +626,6 @@
|
||||||
transform-origin: 50% 50%;
|
transform-origin: 50% 50%;
|
||||||
transform: scale(var(--scale));
|
transform: scale(var(--scale));
|
||||||
user-select: none;
|
user-select: none;
|
||||||
padding: var(--dragPadding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrap {
|
.content-wrap {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
Tags,
|
Tags,
|
||||||
Tag,
|
Tag,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { AutomationActionStepId } from "@budibase/types"
|
import { AutomationActionStepId, BlockDefinitionTypes } from "@budibase/types"
|
||||||
import { automationStore, selectedAutomation } from "@/stores/builder"
|
import { automationStore, selectedAutomation } from "@/stores/builder"
|
||||||
import { admin, licensing } from "@/stores/portal"
|
import { admin, licensing } from "@/stores/portal"
|
||||||
import { externalActions } from "./ExternalActions"
|
import { externalActions } from "./ExternalActions"
|
||||||
|
@ -124,15 +124,20 @@
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newBlock = automationStore.actions.constructBlock(
|
const newBlock = automationStore.actions.constructBlock(
|
||||||
"ACTION",
|
BlockDefinitionTypes.ACTION,
|
||||||
action.stepId,
|
action.stepId,
|
||||||
action
|
action
|
||||||
)
|
)
|
||||||
|
|
||||||
await automationStore.actions.addBlockToAutomation(
|
await automationStore.actions.addBlockToAutomation(
|
||||||
newBlock,
|
newBlock,
|
||||||
blockRef ? blockRef.pathTo : block.pathTo
|
blockRef ? blockRef.pathTo : block.pathTo
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Determine presence of the block before focusing
|
||||||
|
const createdBlock = $selectedAutomation.blockRefs[newBlock.id]
|
||||||
|
const createdBlockLoc = (createdBlock?.pathTo || []).at(-1)
|
||||||
|
await automationStore.actions.selectNode(createdBlockLoc?.id)
|
||||||
|
|
||||||
modal.hide()
|
modal.hide()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
@ -167,8 +172,8 @@
|
||||||
<img
|
<img
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
src={getExternalAction(action.stepId).icon}
|
src={getExternalAction(action.stepId)?.icon}
|
||||||
alt={getExternalAction(action.stepId).name}
|
alt={getExternalAction(action.stepId)?.name}
|
||||||
/>
|
/>
|
||||||
<span class="icon-spacing">
|
<span class="icon-spacing">
|
||||||
<Body size="XS">
|
<Body size="XS">
|
||||||
|
|
|
@ -3,30 +3,28 @@
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
ActionButton,
|
|
||||||
Icon,
|
Icon,
|
||||||
Layout,
|
|
||||||
Body,
|
Body,
|
||||||
Divider,
|
Divider,
|
||||||
TooltipPosition,
|
|
||||||
TooltipType,
|
|
||||||
Button,
|
Button,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import PropField from "@/components/automation/SetupPanel/PropField.svelte"
|
import PropField from "@/components/automation/SetupPanel/PropField.svelte"
|
||||||
import AutomationBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte"
|
import AutomationBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte"
|
||||||
import FlowItemHeader from "./FlowItemHeader.svelte"
|
|
||||||
import FlowItemActions from "./FlowItemActions.svelte"
|
import FlowItemActions from "./FlowItemActions.svelte"
|
||||||
|
import FlowItemStatus from "./FlowItemStatus.svelte"
|
||||||
import {
|
import {
|
||||||
automationStore,
|
automationStore,
|
||||||
selectedAutomation,
|
selectedAutomation,
|
||||||
evaluationContext,
|
evaluationContext,
|
||||||
|
contextMenuStore,
|
||||||
} from "@/stores/builder"
|
} from "@/stores/builder"
|
||||||
import { QueryUtils, Utils, memo } from "@budibase/frontend-core"
|
import { QueryUtils, Utils, memo } from "@budibase/frontend-core"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
import DragZone from "./DragZone.svelte"
|
import DragZone from "./DragZone.svelte"
|
||||||
|
import BlockHeader from "../../SetupPanel/BlockHeader.svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -41,7 +39,6 @@
|
||||||
const memoContext = memo({})
|
const memoContext = memo({})
|
||||||
|
|
||||||
let drawer
|
let drawer
|
||||||
let open = true
|
|
||||||
let confirmDeleteModal
|
let confirmDeleteModal
|
||||||
|
|
||||||
$: memoContext.set($evaluationContext)
|
$: memoContext.set($evaluationContext)
|
||||||
|
@ -61,6 +58,65 @@
|
||||||
branchNode: true,
|
branchNode: true,
|
||||||
pathTo: (pathTo || []).concat({ branchIdx, branchStepId: step.id }),
|
pathTo: (pathTo || []).concat({ branchIdx, branchStepId: step.id }),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getContextMenuItems = () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
icon: "Delete",
|
||||||
|
name: "Delete",
|
||||||
|
keyBind: null,
|
||||||
|
visible: true,
|
||||||
|
disabled: false,
|
||||||
|
callback: async () => {
|
||||||
|
const branchSteps = step.inputs?.children[branch.id]
|
||||||
|
if (branchSteps.length) {
|
||||||
|
confirmDeleteModal.show()
|
||||||
|
} else {
|
||||||
|
await automationStore.actions.deleteBranch(
|
||||||
|
branchBlockRef.pathTo,
|
||||||
|
$selectedAutomation.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "ArrowLeft",
|
||||||
|
name: "Move left",
|
||||||
|
keyBind: null,
|
||||||
|
visible: true,
|
||||||
|
disabled: branchIdx == 0,
|
||||||
|
callback: async () => {
|
||||||
|
automationStore.actions.branchLeft(
|
||||||
|
branchBlockRef.pathTo,
|
||||||
|
$selectedAutomation.data,
|
||||||
|
step
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "ArrowRight",
|
||||||
|
name: "Move right",
|
||||||
|
keyBind: null,
|
||||||
|
visible: true,
|
||||||
|
disabled: isLast,
|
||||||
|
callback: async () => {
|
||||||
|
automationStore.actions.branchRight(
|
||||||
|
branchBlockRef.pathTo,
|
||||||
|
$selectedAutomation.data,
|
||||||
|
step
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const openContextMenu = e => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
const items = getContextMenuItems()
|
||||||
|
contextMenuStore.open(branch.id, items, { x: e.clientX, y: e.clientY })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={confirmDeleteModal}>
|
<Modal bind:this={confirmDeleteModal}>
|
||||||
|
@ -112,7 +168,7 @@
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<div class="flow-item">
|
<div class="flow-item branch">
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
class={`block branch-node hoverable`}
|
class={`block branch-node hoverable`}
|
||||||
|
@ -121,96 +177,64 @@
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FlowItemHeader
|
<div class="block-float">
|
||||||
{automation}
|
<FlowItemStatus
|
||||||
{open}
|
block={step}
|
||||||
itemName={branch.name}
|
{automation}
|
||||||
block={step}
|
{branch}
|
||||||
deleteStep={async () => {
|
hideStatus={$view?.dragging}
|
||||||
const branchSteps = step.inputs?.children[branch.id]
|
/>
|
||||||
if (branchSteps.length) {
|
</div>
|
||||||
confirmDeleteModal.show()
|
<div class="blockSection">
|
||||||
} else {
|
<div class="heading">
|
||||||
await automationStore.actions.deleteBranch(
|
<BlockHeader
|
||||||
branchBlockRef.pathTo,
|
{automation}
|
||||||
$selectedAutomation.data
|
block={step}
|
||||||
)
|
itemName={branch.name}
|
||||||
}
|
on:update={async e => {
|
||||||
}}
|
let stepUpdate = cloneDeep(step)
|
||||||
on:update={async e => {
|
let branchUpdate = stepUpdate.inputs?.branches.find(
|
||||||
let stepUpdate = cloneDeep(step)
|
stepBranch => stepBranch.id == branch.id
|
||||||
let branchUpdate = stepUpdate.inputs?.branches.find(
|
)
|
||||||
stepBranch => stepBranch.id == branch.id
|
branchUpdate.name = e.detail
|
||||||
)
|
|
||||||
branchUpdate.name = e.detail
|
|
||||||
|
|
||||||
const updatedAuto = automationStore.actions.updateStep(
|
const updatedAuto = automationStore.actions.updateStep(
|
||||||
pathTo,
|
pathTo,
|
||||||
$selectedAutomation.data,
|
|
||||||
stepUpdate
|
|
||||||
)
|
|
||||||
await automationStore.actions.save(updatedAuto)
|
|
||||||
}}
|
|
||||||
on:toggle={() => (open = !open)}
|
|
||||||
>
|
|
||||||
<div slot="custom-actions" class="branch-actions">
|
|
||||||
<Icon
|
|
||||||
on:click={() => {
|
|
||||||
automationStore.actions.branchLeft(
|
|
||||||
branchBlockRef.pathTo,
|
|
||||||
$selectedAutomation.data,
|
$selectedAutomation.data,
|
||||||
step
|
stepUpdate
|
||||||
)
|
)
|
||||||
|
await automationStore.actions.save(updatedAuto)
|
||||||
}}
|
}}
|
||||||
tooltip={"Move left"}
|
|
||||||
tooltipType={TooltipType.Info}
|
|
||||||
tooltipPosition={TooltipPosition.Top}
|
|
||||||
hoverable
|
|
||||||
disabled={branchIdx == 0}
|
|
||||||
name="ArrowLeft"
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
on:click={() => {
|
|
||||||
automationStore.actions.branchRight(
|
|
||||||
branchBlockRef.pathTo,
|
|
||||||
$selectedAutomation.data,
|
|
||||||
step
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
tooltip={"Move right"}
|
|
||||||
tooltipType={TooltipType.Info}
|
|
||||||
tooltipPosition={TooltipPosition.Top}
|
|
||||||
hoverable
|
|
||||||
disabled={isLast}
|
|
||||||
name="ArrowRight"
|
|
||||||
/>
|
/>
|
||||||
|
<div class="actions">
|
||||||
|
<Icon
|
||||||
|
name="Info"
|
||||||
|
tooltip="Branch sequencing checks each option in order and follows the first one that matches the rules."
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
on:click={e => {
|
||||||
|
openContextMenu(e)
|
||||||
|
}}
|
||||||
|
size="S"
|
||||||
|
hoverable
|
||||||
|
name="MoreSmallList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FlowItemHeader>
|
</div>
|
||||||
{#if open}
|
|
||||||
<Divider noMargin />
|
<Divider noMargin />
|
||||||
<div class="blockSection">
|
<div class="blockSection filter-button">
|
||||||
<!-- Content body for possible slot -->
|
<PropField label="Only run when:" fullWidth>
|
||||||
<Layout noPadding>
|
<div style="width: 100%">
|
||||||
<PropField label="Only run when">
|
<Button secondary on:click={drawer.show}>
|
||||||
<ActionButton fullWidth on:click={drawer.show}>
|
{editableConditionUI?.groups?.length
|
||||||
{editableConditionUI?.groups?.length
|
? "Update condition"
|
||||||
? "Update condition"
|
: "Add condition"}
|
||||||
: "Add condition"}
|
</Button>
|
||||||
</ActionButton>
|
</div>
|
||||||
</PropField>
|
</PropField>
|
||||||
<div class="footer">
|
</div>
|
||||||
<Icon
|
|
||||||
name="InfoOutline"
|
|
||||||
size="S"
|
|
||||||
color="var(--spectrum-global-color-gray-700)"
|
|
||||||
/>
|
|
||||||
<Body size="XS" color="var(--spectrum-global-color-gray-700)">
|
|
||||||
Only the first branch which matches its condition will run
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="separator" />
|
<div class="separator" />
|
||||||
|
@ -227,6 +251,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.filter-button :global(.spectrum-Button) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.branch-actions {
|
.branch-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
|
@ -264,7 +291,7 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.block {
|
.block {
|
||||||
width: 480px;
|
width: 360px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
@ -289,4 +316,30 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.branch-node {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-float {
|
||||||
|
pointer-events: none;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: -35px;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blockSection .heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blockSection .heading .actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import DiscordLogo from "assets/discord.svg"
|
|
||||||
import ZapierLogo from "assets/zapier.png"
|
|
||||||
import n8nLogo from "assets/n8n_square.png"
|
|
||||||
import MakeLogo from "assets/make.svg"
|
|
||||||
import SlackLogo from "assets/slack.svg"
|
|
||||||
|
|
||||||
export const externalActions = {
|
|
||||||
zapier: { name: "zapier", icon: ZapierLogo },
|
|
||||||
discord: { name: "discord", icon: DiscordLogo },
|
|
||||||
slack: { name: "slack", icon: SlackLogo },
|
|
||||||
integromat: { name: "integromat", icon: MakeLogo },
|
|
||||||
n8n: { name: "n8n", icon: n8nLogo },
|
|
||||||
}
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import DiscordLogo from "assets/discord.svg"
|
||||||
|
import ZapierLogo from "assets/zapier.png"
|
||||||
|
import n8nLogo from "assets/n8n_square.png"
|
||||||
|
import MakeLogo from "assets/make.svg"
|
||||||
|
import SlackLogo from "assets/slack.svg"
|
||||||
|
import { AutomationActionStepId } from "@budibase/types"
|
||||||
|
|
||||||
|
export type ExternalAction = {
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
export const externalActions: Partial<
|
||||||
|
Record<AutomationActionStepId, ExternalAction>
|
||||||
|
> = {
|
||||||
|
[AutomationActionStepId.zapier]: { name: "zapier", icon: ZapierLogo },
|
||||||
|
[AutomationActionStepId.discord]: { name: "discord", icon: DiscordLogo },
|
||||||
|
[AutomationActionStepId.slack]: { name: "slack", icon: SlackLogo },
|
||||||
|
[AutomationActionStepId.integromat]: { name: "integromat", icon: MakeLogo },
|
||||||
|
[AutomationActionStepId.n8n]: { name: "n8n", icon: n8nLogo },
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
automationStore,
|
automationStore,
|
||||||
automationHistoryStore,
|
automationHistoryStore,
|
||||||
selectedAutomation,
|
selectedAutomation,
|
||||||
|
appStore,
|
||||||
} from "@/stores/builder"
|
} from "@/stores/builder"
|
||||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||||
import TestDataModal from "./TestDataModal.svelte"
|
import TestDataModal from "./TestDataModal.svelte"
|
||||||
|
@ -19,6 +20,9 @@
|
||||||
import { memo } from "@budibase/frontend-core"
|
import { memo } from "@budibase/frontend-core"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
import DraggableCanvas from "../DraggableCanvas.svelte"
|
import DraggableCanvas from "../DraggableCanvas.svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { environment } from "@/stores/portal"
|
||||||
|
import Count from "../../SetupPanel/Count.svelte"
|
||||||
|
|
||||||
export let automation
|
export let automation
|
||||||
|
|
||||||
|
@ -31,6 +35,10 @@
|
||||||
let treeEle
|
let treeEle
|
||||||
let draggable
|
let draggable
|
||||||
|
|
||||||
|
let prodErrors
|
||||||
|
|
||||||
|
$: $automationStore.showTestModal === true && testDataModal.show()
|
||||||
|
|
||||||
// Memo auto - selectedAutomation
|
// Memo auto - selectedAutomation
|
||||||
$: memoAutomation.set(automation)
|
$: memoAutomation.set(automation)
|
||||||
|
|
||||||
|
@ -63,53 +71,65 @@
|
||||||
notifications.error("Error deleting automation")
|
notifications.error("Error deleting automation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
await automationStore.actions.initAppSelf()
|
||||||
|
await environment.loadVariables()
|
||||||
|
const response = await automationStore.actions.getLogs({
|
||||||
|
automationId: automation._id,
|
||||||
|
status: "error",
|
||||||
|
})
|
||||||
|
prodErrors = response?.data?.length || 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="header" class:scrolling>
|
<div class="automation-heading">
|
||||||
<div class="header-left">
|
<div class="actions-left">
|
||||||
<UndoRedoControl store={automationHistoryStore} showButtonGroup />
|
<div class="automation-name">
|
||||||
|
{automation.name}
|
||||||
<div class="zoom">
|
|
||||||
<div class="group">
|
|
||||||
<ActionButton icon="Add" quiet on:click={draggable.zoomIn} />
|
|
||||||
<ActionButton icon="Remove" quiet on:click={draggable.zoomOut} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
|
||||||
secondary
|
|
||||||
on:click={() => {
|
|
||||||
draggable.zoomToFit()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Zoom to fit
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="actions-right">
|
||||||
<Button
|
<ActionButton
|
||||||
icon={"Play"}
|
icon="Play"
|
||||||
cta
|
quiet
|
||||||
disabled={!automation?.definition?.trigger}
|
disabled={!automation?.definition?.trigger}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
testDataModal.show()
|
automationStore.update(state => ({ ...state, showTestModal: true }))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Run test
|
Run test
|
||||||
</Button>
|
</ActionButton>
|
||||||
<div class="buttons">
|
<Count
|
||||||
{#if !$automationStore.showTestPanel && $automationStore.testResults}
|
count={prodErrors}
|
||||||
<Button
|
tooltip={"There are errors in production"}
|
||||||
secondary
|
hoverable={false}
|
||||||
icon={"Multiple"}
|
>
|
||||||
disabled={!$automationStore.testResults}
|
<ActionButton
|
||||||
on:click={() => {
|
icon="Folder"
|
||||||
$automationStore.showTestPanel = true
|
quiet
|
||||||
}}
|
selected={prodErrors}
|
||||||
>
|
on:click={() => {
|
||||||
Test details
|
const params = new URLSearchParams({
|
||||||
</Button>
|
...(prodErrors ? { open: "error" } : {}),
|
||||||
{/if}
|
automationId: automation._id,
|
||||||
</div>
|
})
|
||||||
|
window.open(
|
||||||
|
`/builder/app/${
|
||||||
|
$appStore.appId
|
||||||
|
}/settings/automations?${params.toString()}`,
|
||||||
|
"_blank"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Logs
|
||||||
|
</ActionButton>
|
||||||
|
</Count>
|
||||||
|
|
||||||
{#if !isRowAction}
|
{#if !isRowAction}
|
||||||
<div class="toggle-active setting-spacing">
|
<div class="toggle-active setting-spacing">
|
||||||
<Toggle
|
<Toggle
|
||||||
|
@ -126,33 +146,59 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="root" bind:this={treeEle}>
|
<div class="main-flow">
|
||||||
<DraggableCanvas
|
<div class="canvas-heading" class:scrolling>
|
||||||
bind:this={draggable}
|
<div class="canvas-controls">
|
||||||
draggableClasses={[
|
<div class="canvas-heading-left">
|
||||||
"main-content",
|
<UndoRedoControl store={automationHistoryStore} showButtonGroup />
|
||||||
"content",
|
|
||||||
"block",
|
<div class="zoom">
|
||||||
"branched",
|
<div class="group">
|
||||||
"branch",
|
<ActionButton icon="Add" quiet on:click={draggable.zoomIn} />
|
||||||
"flow-item",
|
<ActionButton icon="Remove" quiet on:click={draggable.zoomOut} />
|
||||||
"branch-wrap",
|
</div>
|
||||||
]}
|
</div>
|
||||||
>
|
|
||||||
<span class="main-content" slot="content">
|
<Button
|
||||||
{#if Object.keys(blockRefs).length}
|
secondary
|
||||||
{#each blocks as block, idx (block.id)}
|
on:click={() => {
|
||||||
<StepNode
|
draggable.zoomToFit()
|
||||||
step={blocks[idx]}
|
}}
|
||||||
stepIdx={idx}
|
>
|
||||||
isLast={blocks?.length - 1 === idx}
|
Zoom to fit
|
||||||
automation={$memoAutomation}
|
</Button>
|
||||||
blocks={blockRefs}
|
</div>
|
||||||
/>
|
</div>
|
||||||
{/each}
|
</div>
|
||||||
{/if}
|
|
||||||
</span>
|
<div class="root" bind:this={treeEle}>
|
||||||
</DraggableCanvas>
|
<DraggableCanvas
|
||||||
|
bind:this={draggable}
|
||||||
|
draggableClasses={[
|
||||||
|
"main-content",
|
||||||
|
"content",
|
||||||
|
"block",
|
||||||
|
"branched",
|
||||||
|
"branch",
|
||||||
|
"flow-item",
|
||||||
|
"branch-wrap",
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
@ -166,11 +212,50 @@
|
||||||
This action cannot be undone.
|
This action cannot be undone.
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
<Modal bind:this={testDataModal} width="30%" zIndex={5}>
|
<Modal
|
||||||
|
bind:this={testDataModal}
|
||||||
|
zIndex={5}
|
||||||
|
on:hide={() => {
|
||||||
|
automationStore.update(state => ({ ...state, showTestModal: false }))
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TestDataModal />
|
<TestDataModal />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.main-flow {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-heading {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--background);
|
||||||
|
padding: var(--spacing-m) var(--spacing-l);
|
||||||
|
box-sizing: border-box;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-heading .actions-right {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-name :global(.spectrum-Heading) {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.toggle-active :global(.spectrum-Switch) {
|
.toggle-active :global(.spectrum-Switch) {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
@ -181,12 +266,12 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left {
|
.canvas-heading-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left :global(div) {
|
.canvas-heading-left :global(div) {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,50 +292,31 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header.scrolling {
|
.canvas-heading.scrolling {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
border-bottom: var(--border-light);
|
border-bottom: var(--border-light);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.canvas-controls {
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--spacing-l);
|
padding: var(--spacing-l);
|
||||||
flex: 0 0 60px;
|
|
||||||
padding-right: var(--spacing-xl);
|
padding-right: var(--spacing-xl);
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header > * {
|
.canvas-controls > * {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.toggle-active :global(.spectrum-Switch-label) {
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-l);
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls .toggle-active :global(.spectrum-Switch-label) {
|
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group {
|
.group {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -266,17 +332,17 @@
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left .group :global(.spectrum-Button),
|
.canvas-heading-left .group :global(.spectrum-Button),
|
||||||
.header-left .group :global(.spectrum-ActionButton),
|
.canvas-heading-left .group :global(.spectrum-ActionButton),
|
||||||
.header-left .group :global(.spectrum-Icon) {
|
.canvas-heading-left .group :global(.spectrum-Icon) {
|
||||||
color: var(--spectrum-global-color-gray-900) !important;
|
color: var(--spectrum-global-color-gray-900) !important;
|
||||||
}
|
}
|
||||||
.header-left .group :global(.spectrum-Button),
|
.canvas-heading-left .group :global(.spectrum-Button),
|
||||||
.header-left .group :global(.spectrum-ActionButton) {
|
.canvas-heading-left .group :global(.spectrum-ActionButton) {
|
||||||
background: var(--spectrum-global-color-gray-200) !important;
|
background: var(--spectrum-global-color-gray-200) !important;
|
||||||
}
|
}
|
||||||
.header-left .group :global(.spectrum-Button:hover),
|
.canvas-heading-left .group :global(.spectrum-Button:hover),
|
||||||
.header-left .group :global(.spectrum-ActionButton:hover) {
|
.canvas-heading-left .group :global(.spectrum-ActionButton:hover) {
|
||||||
background: var(--spectrum-global-color-gray-300) !important;
|
background: var(--spectrum-global-color-gray-300) !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,38 +1,21 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { automationStore, selectedAutomation, tables } from "@/stores/builder"
|
||||||
automationStore,
|
import { Modal } from "@budibase/bbui"
|
||||||
permissions,
|
|
||||||
selectedAutomation,
|
|
||||||
tables,
|
|
||||||
} from "@/stores/builder"
|
|
||||||
import {
|
|
||||||
Icon,
|
|
||||||
Divider,
|
|
||||||
Layout,
|
|
||||||
Detail,
|
|
||||||
Modal,
|
|
||||||
Label,
|
|
||||||
AbsTooltip,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
|
||||||
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import FlowItemHeader from "./FlowItemHeader.svelte"
|
import { ActionStepID } from "@/constants/backend/automations"
|
||||||
import RoleSelect from "@/components/design/settings/controls/RoleSelect.svelte"
|
|
||||||
import { ActionStepID, TriggerStepID } from "@/constants/backend/automations"
|
|
||||||
import { AutomationStepType } from "@budibase/types"
|
import { AutomationStepType } from "@budibase/types"
|
||||||
import FlowItemActions from "./FlowItemActions.svelte"
|
import FlowItemActions from "./FlowItemActions.svelte"
|
||||||
|
import FlowItemStatus from "./FlowItemStatus.svelte"
|
||||||
import DragHandle from "@/components/design/settings/controls/DraggableList/drag-handle.svelte"
|
import DragHandle from "@/components/design/settings/controls/DraggableList/drag-handle.svelte"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import DragZone from "./DragZone.svelte"
|
import DragZone from "./DragZone.svelte"
|
||||||
import InfoDisplay from "@/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
import InfoDisplay from "@/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
||||||
|
import BlockHeader from "../../SetupPanel/BlockHeader.svelte"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let blockRef
|
export let blockRef
|
||||||
export let testDataModal
|
|
||||||
export let idx
|
|
||||||
export let automation
|
export let automation
|
||||||
export let bindings
|
|
||||||
export let draggable = true
|
export let draggable = true
|
||||||
|
|
||||||
const view = getContext("draggableView")
|
const view = getContext("draggableView")
|
||||||
|
@ -40,13 +23,47 @@
|
||||||
const contentPos = getContext("contentPos")
|
const contentPos = getContext("contentPos")
|
||||||
|
|
||||||
let webhookModal
|
let webhookModal
|
||||||
let open = true
|
|
||||||
let showLooping = false
|
|
||||||
let role
|
|
||||||
let blockEle
|
let blockEle
|
||||||
let positionStyles
|
let positionStyles
|
||||||
let blockDims
|
let blockDims
|
||||||
|
|
||||||
|
$: pathSteps = loadSteps(blockRef)
|
||||||
|
|
||||||
|
$: collectBlockExists = pathSteps.some(
|
||||||
|
step => step.stepId === ActionStepID.COLLECT
|
||||||
|
)
|
||||||
|
|
||||||
|
$: isTrigger = block.type === AutomationStepType.TRIGGER
|
||||||
|
$: lastStep = blockRef?.terminating
|
||||||
|
|
||||||
|
$: triggerInfo = sdk.automations.isRowAction($selectedAutomation?.data) && {
|
||||||
|
title: "Automation trigger",
|
||||||
|
tableName: $tables.list.find(
|
||||||
|
x =>
|
||||||
|
x._id === $selectedAutomation.data?.definition?.trigger?.inputs?.tableId
|
||||||
|
)?.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
$: selectedNodeId = $automationStore.selectedNodeId
|
||||||
|
$: selected = block.id === selectedNodeId
|
||||||
|
$: dragging = $view?.moveStep && $view?.moveStep?.id === block.id
|
||||||
|
|
||||||
|
$: if (dragging && blockEle) {
|
||||||
|
updateBlockDims()
|
||||||
|
}
|
||||||
|
|
||||||
|
$: placeholderDims = buildPlaceholderStyles(blockDims)
|
||||||
|
|
||||||
|
// Move the selected item
|
||||||
|
// Listen for scrolling in the content. As its scrolled this will be updated
|
||||||
|
$: move(
|
||||||
|
blockEle,
|
||||||
|
$view?.dragSpot,
|
||||||
|
dragging,
|
||||||
|
$contentPos?.scrollX,
|
||||||
|
$contentPos?.scrollY
|
||||||
|
)
|
||||||
|
|
||||||
const updateBlockDims = () => {
|
const updateBlockDims = () => {
|
||||||
if (!blockEle) {
|
if (!blockEle) {
|
||||||
return
|
return
|
||||||
|
@ -66,48 +83,8 @@
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
|
|
||||||
$: pathSteps = loadSteps(blockRef)
|
const move = (block, dragPos, dragging, scrollX, scrollY) => {
|
||||||
|
if ((!block && !dragging) || !dragPos) {
|
||||||
$: collectBlockExists = pathSteps.some(
|
|
||||||
step => step.stepId === ActionStepID.COLLECT
|
|
||||||
)
|
|
||||||
$: automationId = automation?._id
|
|
||||||
$: isTrigger = block.type === AutomationStepType.TRIGGER
|
|
||||||
$: lastStep = blockRef?.terminating
|
|
||||||
|
|
||||||
$: loopBlock = pathSteps.find(x => x.blockToLoop === block.id)
|
|
||||||
$: isAppAction = block?.stepId === TriggerStepID.APP
|
|
||||||
$: isAppAction && setPermissions(role)
|
|
||||||
$: isAppAction && getPermissions(automationId)
|
|
||||||
|
|
||||||
$: triggerInfo = sdk.automations.isRowAction($selectedAutomation?.data) && {
|
|
||||||
title: "Automation trigger",
|
|
||||||
tableName: $tables.list.find(
|
|
||||||
x =>
|
|
||||||
x._id === $selectedAutomation.data?.definition?.trigger?.inputs?.tableId
|
|
||||||
)?.name,
|
|
||||||
}
|
|
||||||
|
|
||||||
$: selected = $view?.moveStep && $view?.moveStep?.id === block.id
|
|
||||||
|
|
||||||
$: if (selected && blockEle) {
|
|
||||||
updateBlockDims()
|
|
||||||
}
|
|
||||||
|
|
||||||
$: placeholderDims = buildPlaceholderStyles(blockDims)
|
|
||||||
|
|
||||||
// Move the selected item
|
|
||||||
// Listen for scrolling in the content. As its scrolled this will be updated
|
|
||||||
$: move(
|
|
||||||
blockEle,
|
|
||||||
$view?.dragSpot,
|
|
||||||
selected,
|
|
||||||
$contentPos?.scrollX,
|
|
||||||
$contentPos?.scrollY
|
|
||||||
)
|
|
||||||
|
|
||||||
const move = (block, dragPos, selected, scrollX, scrollY) => {
|
|
||||||
if ((!block && !selected) || !dragPos) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
positionStyles = `
|
positionStyles = `
|
||||||
|
@ -125,52 +102,6 @@
|
||||||
--psheight: ${Math.round(height)}px;`
|
--psheight: ${Math.round(height)}px;`
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setPermissions(role) {
|
|
||||||
if (!role || !automationId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await permissions.save({
|
|
||||||
level: "execute",
|
|
||||||
role,
|
|
||||||
resource: automationId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getPermissions(automationId) {
|
|
||||||
if (!automationId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const perms = await permissions.forResource(automationId)
|
|
||||||
if (!perms["execute"]) {
|
|
||||||
role = "BASIC"
|
|
||||||
} else {
|
|
||||||
role = perms["execute"].role
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteStep() {
|
|
||||||
await automationStore.actions.deleteAutomationBlock(blockRef.pathTo)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeLooping() {
|
|
||||||
let loopBlockRef = $selectedAutomation.blockRefs[blockRef.looped]
|
|
||||||
await automationStore.actions.deleteAutomationBlock(loopBlockRef.pathTo)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addLooping() {
|
|
||||||
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
|
|
||||||
const loopBlock = automationStore.actions.constructBlock(
|
|
||||||
"ACTION",
|
|
||||||
"LOOP",
|
|
||||||
loopDefinition
|
|
||||||
)
|
|
||||||
loopBlock.blockToLoop = block.id
|
|
||||||
await automationStore.actions.addBlockToAutomation(
|
|
||||||
loopBlock,
|
|
||||||
blockRef.pathTo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onHandleMouseDown = e => {
|
const onHandleMouseDown = e => {
|
||||||
if (isTrigger) {
|
if (isTrigger) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
@ -205,143 +136,58 @@
|
||||||
<div
|
<div
|
||||||
id={`block-${block.id}`}
|
id={`block-${block.id}`}
|
||||||
class={`block ${block.type} hoverable`}
|
class={`block ${block.type} hoverable`}
|
||||||
class:selected
|
class:dragging
|
||||||
class:draggable
|
class:draggable
|
||||||
|
class:selected
|
||||||
>
|
>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
{#if $view.dragging && selected}
|
{#if $view.dragging && dragging}
|
||||||
<div class="drag-placeholder" style={placeholderDims} />
|
<div class="drag-placeholder" style={placeholderDims} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={blockEle}
|
bind:this={blockEle}
|
||||||
class="block-content"
|
class="block-content"
|
||||||
class:dragging={$view.dragging && selected}
|
class:dragging={$view.dragging && dragging}
|
||||||
style={positionStyles}
|
style={positionStyles}
|
||||||
on:mousedown={e => {
|
on:mousedown={e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div class="block-float">
|
||||||
|
<FlowItemStatus {block} {automation} hideStatus={$view?.dragging} />
|
||||||
|
</div>
|
||||||
{#if draggable}
|
{#if draggable}
|
||||||
<div
|
<div
|
||||||
class="handle"
|
class="handle"
|
||||||
class:grabbing={selected}
|
class:grabbing={dragging}
|
||||||
on:mousedown={onHandleMouseDown}
|
on:mousedown={onHandleMouseDown}
|
||||||
>
|
>
|
||||||
<DragHandle />
|
<DragHandle />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="block-core">
|
<div
|
||||||
{#if loopBlock}
|
class="block-core"
|
||||||
|
on:click={async () => {
|
||||||
|
await automationStore.actions.selectNode(block.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="blockSection block-info">
|
||||||
|
<BlockHeader
|
||||||
|
disabled
|
||||||
|
{automation}
|
||||||
|
{block}
|
||||||
|
on:update={e =>
|
||||||
|
automationStore.actions.updateBlockTitle(block, e.detail)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if isTrigger && triggerInfo}
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
<div
|
<InfoDisplay
|
||||||
on:click={() => {
|
title={triggerInfo.title}
|
||||||
showLooping = !showLooping
|
body="This trigger is tied to your '{triggerInfo.tableName}' table"
|
||||||
}}
|
icon="InfoOutline"
|
||||||
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}
|
|
||||||
<InfoDisplay
|
|
||||||
title={triggerInfo.title}
|
|
||||||
body="This trigger is tied to your '{triggerInfo.tableName}' table"
|
|
||||||
icon="InfoOutline"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -397,7 +243,7 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.block {
|
.block {
|
||||||
width: 480px;
|
width: 360px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
@ -419,6 +265,8 @@
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
color: var(--grey-6);
|
color: var(--grey-6);
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
}
|
}
|
||||||
.block.draggable .wrap .handle.grabbing {
|
.block.draggable .wrap .handle.grabbing {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
|
@ -469,4 +317,22 @@
|
||||||
top: var(--blockPosY);
|
top: var(--blockPosY);
|
||||||
left: var(--blockPosX);
|
left: var(--blockPosX);
|
||||||
}
|
}
|
||||||
|
.block-float {
|
||||||
|
pointer-events: none;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: -35px;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
.block-core {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.block.selected .block-content {
|
||||||
|
border-color: var(--spectrum-global-color-blue-700);
|
||||||
|
transition: border 130ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-info {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { automationStore, selectedAutomation } from "@/stores/builder"
|
||||||
|
import {
|
||||||
|
type FlowItemStatus,
|
||||||
|
DataMode,
|
||||||
|
FilterableRowTriggers,
|
||||||
|
FlowStatusType,
|
||||||
|
} from "@/types/automations"
|
||||||
|
import {
|
||||||
|
type AutomationStep,
|
||||||
|
type AutomationStepResult,
|
||||||
|
type AutomationTrigger,
|
||||||
|
type AutomationTriggerResult,
|
||||||
|
type Branch,
|
||||||
|
type DidNotTriggerResponse,
|
||||||
|
type TestAutomationResponse,
|
||||||
|
type AutomationTriggerStepId,
|
||||||
|
isDidNotTriggerResponse,
|
||||||
|
isTrigger,
|
||||||
|
isBranchStep,
|
||||||
|
isFilterStep,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { ActionButton } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let block: AutomationStep | AutomationTrigger | undefined
|
||||||
|
export let branch: Branch | undefined
|
||||||
|
export let hideStatus: boolean | undefined = false
|
||||||
|
|
||||||
|
$: blockRef = block?.id ? $selectedAutomation.blockRefs[block?.id] : null
|
||||||
|
$: isTriggerBlock = block ? isTrigger(block) : false
|
||||||
|
|
||||||
|
$: testResults = $automationStore.testResults as TestAutomationResponse
|
||||||
|
$: blockResult = automationStore.actions.processBlockResults(
|
||||||
|
testResults,
|
||||||
|
block
|
||||||
|
)
|
||||||
|
$: flowStatus = getFlowStatus(blockResult)
|
||||||
|
|
||||||
|
const getFlowStatus = (
|
||||||
|
result?:
|
||||||
|
| AutomationTriggerResult
|
||||||
|
| AutomationStepResult
|
||||||
|
| DidNotTriggerResponse
|
||||||
|
): FlowItemStatus | undefined => {
|
||||||
|
if (!result || !block) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const outputs = result?.outputs
|
||||||
|
const isFilteredRowTrigger =
|
||||||
|
isTriggerBlock &&
|
||||||
|
isDidNotTriggerResponse(testResults) &&
|
||||||
|
FilterableRowTriggers.includes(block.stepId as AutomationTriggerStepId)
|
||||||
|
|
||||||
|
if (
|
||||||
|
isFilteredRowTrigger ||
|
||||||
|
(isFilterStep(block) && "result" in outputs && outputs.result === false)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
message: "Stopped",
|
||||||
|
icon: "Alert",
|
||||||
|
type: FlowStatusType.WARN,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (branch && isBranchStep(block)) {
|
||||||
|
// Do not give status markers to branch nodes that were not part of the run.
|
||||||
|
if ("branchId" in outputs && outputs.branchId !== branch.id) return
|
||||||
|
|
||||||
|
// Mark branches as stopped when no branch criteria was met
|
||||||
|
if (outputs.success == false) {
|
||||||
|
return {
|
||||||
|
message: "Stopped",
|
||||||
|
icon: "Alert",
|
||||||
|
type: FlowStatusType.WARN,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = outputs?.success || isTriggerBlock
|
||||||
|
return {
|
||||||
|
message: success ? "Completed" : "Failed",
|
||||||
|
icon: success ? "CheckmarkCircle" : "AlertCircleFilled",
|
||||||
|
type:
|
||||||
|
success || isTriggerBlock
|
||||||
|
? FlowStatusType.SUCCESS
|
||||||
|
: FlowStatusType.ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flow-item-status">
|
||||||
|
{#if blockRef}
|
||||||
|
{#if isTriggerBlock}
|
||||||
|
<span class="block-type">
|
||||||
|
<ActionButton size="S" active={false} icon="Workflow">
|
||||||
|
Trigger
|
||||||
|
</ActionButton>
|
||||||
|
</span>
|
||||||
|
{:else if blockRef.looped}
|
||||||
|
<ActionButton size="S" active={false} icon="Reuse">Looping</ActionButton>
|
||||||
|
{:else}
|
||||||
|
<span />
|
||||||
|
{/if}
|
||||||
|
{#if blockResult && flowStatus && !hideStatus}
|
||||||
|
<span class={`flow-${flowStatus.type}`}>
|
||||||
|
<ActionButton
|
||||||
|
size="S"
|
||||||
|
icon={flowStatus.icon}
|
||||||
|
tooltip={flowStatus?.tooltip}
|
||||||
|
on:click={async () => {
|
||||||
|
if (branch || !block) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await automationStore.actions.selectNode(
|
||||||
|
block?.id,
|
||||||
|
flowStatus.type == FlowStatusType.SUCCESS
|
||||||
|
? DataMode.OUTPUT
|
||||||
|
: DataMode.ERRORS
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{flowStatus.message}
|
||||||
|
</ActionButton>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.flow-item-status {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
align-items: center;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.flow-item-status :global(> *) {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
.flow-item-status .block-type {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.flow-item-status :global(.spectrum-ActionButton),
|
||||||
|
.flow-item-status :global(.spectrum-ActionButton .spectrum-Icon) {
|
||||||
|
color: var(--spectrum-alias-text-color-hover);
|
||||||
|
}
|
||||||
|
.flow-success :global(.spectrum-ActionButton) {
|
||||||
|
background-color: var(--spectrum-semantic-positive-color-status);
|
||||||
|
border-color: var(--spectrum-semantic-positive-color-status);
|
||||||
|
}
|
||||||
|
.flow-error :global(.spectrum-ActionButton) {
|
||||||
|
background-color: var(--spectrum-semantic-negative-color-status);
|
||||||
|
border-color: var(--spectrum-semantic-negative-color-status);
|
||||||
|
}
|
||||||
|
.flow-warn :global(.spectrum-ActionButton) {
|
||||||
|
background-color: var(--spectrum-global-color-gray-300);
|
||||||
|
border-color: var(--spectrum-global-color-gray-300);
|
||||||
|
}
|
||||||
|
.flow-warn :global(.spectrum-ActionButton .spectrum-Icon) {
|
||||||
|
color: var(--spectrum-global-color-yellow-600);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -46,16 +46,26 @@
|
||||||
...userBindings,
|
...userBindings,
|
||||||
...settingBindings,
|
...settingBindings,
|
||||||
]
|
]
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Register the trigger as the focus element for the automation
|
// Register the trigger as the focus element for the automation
|
||||||
// Onload, the canvas will use the dimensions to center the step
|
// Onload, the canvas will use the dimensions to center the step
|
||||||
if (stepEle && step.type === "TRIGGER" && !$view.focusEle) {
|
if (stepEle && step.type === "TRIGGER" && !$view.focusEle) {
|
||||||
const { width, height, left, right, top, bottom, x, y } =
|
const { width, height, left, right, top, bottom, x, y } =
|
||||||
stepEle.getBoundingClientRect()
|
stepEle.getBoundingClientRect()
|
||||||
|
|
||||||
view.update(state => ({
|
view.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
focusEle: { width, height, left, right, top, bottom, x, y },
|
focusEle: {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
top,
|
||||||
|
bottom,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
...(step.type === "TRIGGER" ? { targetY: 100 } : {}),
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -122,7 +122,6 @@
|
||||||
await tick()
|
await tick()
|
||||||
try {
|
try {
|
||||||
await automationStore.actions.test($selectedAutomation.data, testData)
|
await automationStore.actions.test($selectedAutomation.data, testData)
|
||||||
$automationStore.showTestPanel = true
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(error)
|
notifications.error(error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,270 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ActionButton, Button, Divider, Icon, Modal } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
type Automation,
|
||||||
|
type AutomationStep,
|
||||||
|
type AutomationTrigger,
|
||||||
|
type BlockRef,
|
||||||
|
AutomationTriggerStepId,
|
||||||
|
isBranchStep,
|
||||||
|
isTrigger,
|
||||||
|
AutomationFeature,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { memo } from "@budibase/frontend-core"
|
||||||
|
import {
|
||||||
|
automationStore,
|
||||||
|
selectedAutomation,
|
||||||
|
evaluationContext,
|
||||||
|
} from "@/stores/builder"
|
||||||
|
import BlockData from "../SetupPanel/BlockData.svelte"
|
||||||
|
import BlockProperties from "../SetupPanel/BlockProperties.svelte"
|
||||||
|
import BlockHeader from "../SetupPanel/BlockHeader.svelte"
|
||||||
|
import { PropField } from "../SetupPanel"
|
||||||
|
import RoleSelect from "@/components/common/RoleSelect.svelte"
|
||||||
|
import { type AutomationContext } from "@/stores/builder/automations"
|
||||||
|
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
|
import { getVerticalResizeActions } from "@/components/common/resizable"
|
||||||
|
|
||||||
|
const [resizable, resizableHandle] = getVerticalResizeActions()
|
||||||
|
|
||||||
|
const memoAutomation = memo<Automation | undefined>($selectedAutomation.data)
|
||||||
|
const memoBlock = memo<AutomationStep | AutomationTrigger | undefined>()
|
||||||
|
const memoContext = memo({} as AutomationContext)
|
||||||
|
|
||||||
|
let role: string | undefined
|
||||||
|
let webhookModal: Modal | undefined
|
||||||
|
|
||||||
|
$: memoAutomation.set($selectedAutomation.data)
|
||||||
|
$: memoContext.set($evaluationContext)
|
||||||
|
|
||||||
|
$: selectedNodeId = $automationStore.selectedNodeId
|
||||||
|
$: blockRefs = $selectedAutomation.blockRefs
|
||||||
|
$: blockRef = selectedNodeId ? blockRefs[selectedNodeId] : undefined
|
||||||
|
$: block = automationStore.actions.getBlockByRef($memoAutomation, blockRef)
|
||||||
|
|
||||||
|
$: memoBlock.set(block)
|
||||||
|
|
||||||
|
$: isStep = !!$memoBlock && !isTrigger($memoBlock)
|
||||||
|
$: pathSteps = loadPathSteps(blockRef, $memoAutomation)
|
||||||
|
$: loopBlock = pathSteps.find(
|
||||||
|
x => $memoBlock && x.blockToLoop === $memoBlock.id
|
||||||
|
)
|
||||||
|
|
||||||
|
$: isAppAction = block?.stepId === AutomationTriggerStepId.APP
|
||||||
|
$: isAppAction && fetchPermissions($memoAutomation?._id)
|
||||||
|
$: isAppAction &&
|
||||||
|
automationStore.actions.setPermissions(role, $memoAutomation)
|
||||||
|
|
||||||
|
const fetchPermissions = async (automationId?: string) => {
|
||||||
|
if (!automationId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
role = await automationStore.actions.getPermissions(automationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadPathSteps = (
|
||||||
|
blockRef: BlockRef | undefined,
|
||||||
|
automation: Automation | undefined
|
||||||
|
) => {
|
||||||
|
return blockRef && automation
|
||||||
|
? automationStore.actions.getPathSteps(blockRef.pathTo, automation)
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={webhookModal}>
|
||||||
|
<CreateWebhookModal />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<div class="panel heading">
|
||||||
|
<div class="details">
|
||||||
|
<BlockHeader
|
||||||
|
automation={$memoAutomation}
|
||||||
|
block={$memoBlock}
|
||||||
|
on:update={e => {
|
||||||
|
if ($memoBlock && !isTrigger($memoBlock)) {
|
||||||
|
automationStore.actions.updateBlockTitle($memoBlock, e.detail)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="Close"
|
||||||
|
hoverable
|
||||||
|
on:click={() => {
|
||||||
|
automationStore.actions.selectNode()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if isStep}
|
||||||
|
<div class="step-actions">
|
||||||
|
{#if $memoBlock && !isBranchStep($memoBlock) && ($memoBlock.features?.[AutomationFeature.LOOPING] || !$memoBlock.features)}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
noPadding
|
||||||
|
icon="RotateCW"
|
||||||
|
on:click={async () => {
|
||||||
|
if (loopBlock) {
|
||||||
|
await automationStore.actions.removeLooping(blockRef)
|
||||||
|
} else {
|
||||||
|
await automationStore.actions.addLooping(blockRef)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loopBlock ? `Stop Looping` : `Loop`}
|
||||||
|
</ActionButton>
|
||||||
|
{/if}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
noPadding
|
||||||
|
icon="DeleteOutline"
|
||||||
|
on:click={async () => {
|
||||||
|
if (!blockRef) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await automationStore.actions.deleteAutomationBlock(blockRef.pathTo)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Divider noMargin />
|
||||||
|
<div class="panel config" use:resizable>
|
||||||
|
<div class="content">
|
||||||
|
{#if loopBlock}
|
||||||
|
<span class="loop">
|
||||||
|
<BlockProperties
|
||||||
|
block={loopBlock}
|
||||||
|
context={$memoContext}
|
||||||
|
automation={$memoAutomation}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<Divider noMargin />
|
||||||
|
{:else if isAppAction}
|
||||||
|
<PropField label="Role" fullWidth>
|
||||||
|
<RoleSelect bind:value={role} />
|
||||||
|
</PropField>
|
||||||
|
{/if}
|
||||||
|
<span class="props">
|
||||||
|
<BlockProperties
|
||||||
|
block={$memoBlock}
|
||||||
|
context={$memoContext}
|
||||||
|
automation={$memoAutomation}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{#if block?.stepId === AutomationTriggerStepId.WEBHOOK}
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
on:click={() => {
|
||||||
|
if (!webhookModal) return
|
||||||
|
webhookModal.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Set Up Webhook
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
role="separator"
|
||||||
|
class="divider"
|
||||||
|
class:disabled={false}
|
||||||
|
use:resizableHandle
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel data">
|
||||||
|
<BlockData
|
||||||
|
context={$memoContext}
|
||||||
|
block={$memoBlock}
|
||||||
|
automation={$memoAutomation}
|
||||||
|
on:run={() => {
|
||||||
|
automationStore.update(state => ({
|
||||||
|
...state,
|
||||||
|
showTestModal: true,
|
||||||
|
}))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.step-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.heading.panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.panel,
|
||||||
|
.config.panel .content {
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.config.panel {
|
||||||
|
padding: 0px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 550px;
|
||||||
|
transition: height 300ms ease-out, max-height 300ms ease-out;
|
||||||
|
height: 400px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.config.panel .content {
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.config.panel .loop,
|
||||||
|
.config.panel .content .props {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.data.panel {
|
||||||
|
padding: 0px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.divider {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
transform: translateY(50%);
|
||||||
|
height: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.divider:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
height: 2px;
|
||||||
|
width: 100%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
transition: background 130ms ease-out;
|
||||||
|
}
|
||||||
|
.divider:hover {
|
||||||
|
cursor: row-resize;
|
||||||
|
}
|
||||||
|
.divider:hover:after {
|
||||||
|
background: var(--spectrum-global-color-gray-300);
|
||||||
|
}
|
||||||
|
.divider.disabled {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.divider.disabled:after {
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
|
.details {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,44 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
type AutomationStep,
|
||||||
|
type AutomationTrigger,
|
||||||
|
type EnrichedBinding,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { PropField } from "."
|
||||||
|
import { type SchemaConfigProps } from "@/types/automations"
|
||||||
|
|
||||||
|
export let block: AutomationStep | AutomationTrigger | undefined = undefined
|
||||||
|
export let context: {} | undefined
|
||||||
|
export let bindings: EnrichedBinding[] | undefined = undefined
|
||||||
|
export let layout: SchemaConfigProps[] | undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if layout}
|
||||||
|
{#each layout as config}
|
||||||
|
{#if config.wrapped === false}
|
||||||
|
<svelte:component
|
||||||
|
this={config.comp}
|
||||||
|
{...config?.props ? config.props() : {}}
|
||||||
|
{bindings}
|
||||||
|
{block}
|
||||||
|
{context}
|
||||||
|
on:change={config.onChange}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<PropField
|
||||||
|
label={config.title}
|
||||||
|
labelTooltip={config.tooltip || ""}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<svelte:component
|
||||||
|
this={config.comp}
|
||||||
|
{...config?.props ? config.props() : {}}
|
||||||
|
{bindings}
|
||||||
|
{block}
|
||||||
|
{context}
|
||||||
|
on:change={config.onChange}
|
||||||
|
/>
|
||||||
|
</PropField>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
|
@ -0,0 +1,389 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
type BaseIOStructure,
|
||||||
|
type AutomationStep,
|
||||||
|
type AutomationTrigger,
|
||||||
|
AutomationActionStepId,
|
||||||
|
AutomationCustomIOType,
|
||||||
|
AutomationIOType,
|
||||||
|
AutomationStepType,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import {
|
||||||
|
AutomationSelector,
|
||||||
|
CronBuilder,
|
||||||
|
DateSelector,
|
||||||
|
ExecuteScript,
|
||||||
|
ExecuteScriptV2,
|
||||||
|
FieldSelector,
|
||||||
|
FileSelector,
|
||||||
|
FilterSelector,
|
||||||
|
PropField,
|
||||||
|
QueryParamSelector,
|
||||||
|
SchemaSetup,
|
||||||
|
TableSelector,
|
||||||
|
} from "."
|
||||||
|
import { getFieldLabel, getInputValue } from "./layouts"
|
||||||
|
import { automationStore, tables } from "@/stores/builder"
|
||||||
|
import {
|
||||||
|
type AutomationSchemaConfig,
|
||||||
|
type FieldProps,
|
||||||
|
type FormUpdate,
|
||||||
|
type FileSelectorMeta,
|
||||||
|
type StepInputs,
|
||||||
|
SchemaFieldTypes,
|
||||||
|
customTypeToSchema,
|
||||||
|
typeToSchema,
|
||||||
|
} from "@/types/automations"
|
||||||
|
import { Select, Checkbox } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
DrawerBindableInput,
|
||||||
|
ServerBindingPanel as AutomationBindingPanel,
|
||||||
|
} from "@/components/common/bindings"
|
||||||
|
import Editor from "@/components/integration/QueryEditor.svelte"
|
||||||
|
import WebhookDisplay from "@/components/automation/Shared/WebhookDisplay.svelte"
|
||||||
|
|
||||||
|
export let block: AutomationStep | AutomationTrigger | undefined = undefined
|
||||||
|
export let context: {} | undefined
|
||||||
|
export let bindings: any[] | undefined = undefined
|
||||||
|
|
||||||
|
const SchemaTypes: AutomationSchemaConfig = {
|
||||||
|
[SchemaFieldTypes.ENUM]: {
|
||||||
|
comp: Select,
|
||||||
|
props: (opts: FieldProps = {} as FieldProps) => {
|
||||||
|
const field = opts.field
|
||||||
|
return {
|
||||||
|
placeholder: false,
|
||||||
|
options: field.enum,
|
||||||
|
getOptionLabel: (x: string, idx: number) => {
|
||||||
|
return field.pretty ? field.pretty[idx] : x
|
||||||
|
},
|
||||||
|
disabled: field.readonly,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.TABLE]: {
|
||||||
|
comp: TableSelector,
|
||||||
|
props: (opts: FieldProps = {} as FieldProps) => {
|
||||||
|
const field = opts.field
|
||||||
|
return {
|
||||||
|
isTrigger,
|
||||||
|
disabled: field.readonly,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.FILTER]: {
|
||||||
|
comp: FilterSelector,
|
||||||
|
props: (opts: FieldProps = {} as FieldProps) => {
|
||||||
|
const { key } = opts
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||||
|
const update = e.detail
|
||||||
|
if (block) {
|
||||||
|
automationStore.actions.requestUpdate(update, block)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.COLUMN]: {
|
||||||
|
comp: Select,
|
||||||
|
props: (opts: FieldProps = {} as FieldProps) => {
|
||||||
|
const field = opts.field
|
||||||
|
return {
|
||||||
|
options: Object.keys(table?.schema || {}),
|
||||||
|
disabled: field.readonly,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.CODE_V2]: {
|
||||||
|
comp: ExecuteScriptV2,
|
||||||
|
fullWidth: true,
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.BOOL]: {
|
||||||
|
comp: Checkbox,
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.DATE]: {
|
||||||
|
comp: DateSelector,
|
||||||
|
props: (opts: FieldProps = {} as FieldProps) => {
|
||||||
|
const { field, key } = opts
|
||||||
|
return {
|
||||||
|
title: field.title ?? getFieldLabel(key, field),
|
||||||
|
disabled: field.readonly,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.TRIGGER_SCHEMA]: {
|
||||||
|
comp: SchemaSetup,
|
||||||
|
fullWidth: true,
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.JSON]: {
|
||||||
|
comp: Editor,
|
||||||
|
props: (opts: FieldProps = {} as FieldProps) => {
|
||||||
|
const { field, value: editorBody } = opts
|
||||||
|
return {
|
||||||
|
value: editorBody?.value,
|
||||||
|
editorHeight: "250",
|
||||||
|
editorWidth: "448",
|
||||||
|
mode: "json",
|
||||||
|
readOnly: field.readonly,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.FILE]: {
|
||||||
|
comp: FileSelector,
|
||||||
|
props: (opts: FieldProps = {} as FieldProps) => {
|
||||||
|
const { field } = opts
|
||||||
|
const meta = getInputValue(inputData, "meta") as FileSelectorMeta
|
||||||
|
return {
|
||||||
|
buttonText:
|
||||||
|
field.type === "attachment" ? "Add attachment" : "Add signature",
|
||||||
|
useAttachmentBinding: meta?.useAttachmentBinding,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||||
|
const update = e.detail
|
||||||
|
if (block) {
|
||||||
|
automationStore.actions.requestUpdate(update, block)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fullWidth: true,
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.CRON]: {
|
||||||
|
comp: CronBuilder,
|
||||||
|
props: (opts: FieldProps = {} as FieldProps) => {
|
||||||
|
const { value } = opts
|
||||||
|
return {
|
||||||
|
cronExpression: value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.LOOP_OPTION]: {
|
||||||
|
comp: Select,
|
||||||
|
props: () => {
|
||||||
|
return {
|
||||||
|
autoWidth: true,
|
||||||
|
options: ["Array", "String"],
|
||||||
|
defaultValue: "Array",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.AUTOMATION_FIELDS]: {
|
||||||
|
comp: AutomationSelector,
|
||||||
|
props: (opts: FieldProps = {} as FieldProps) => {
|
||||||
|
const { field } = opts
|
||||||
|
return {
|
||||||
|
title: field.title,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fullWidth: true,
|
||||||
|
wrapped: false,
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.WEBHOOK_URL]: {
|
||||||
|
comp: WebhookDisplay,
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.QUERY_PARAMS]: {
|
||||||
|
comp: QueryParamSelector,
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.CODE]: {
|
||||||
|
comp: ExecuteScript,
|
||||||
|
fullWidth: true,
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.STRING]: {
|
||||||
|
comp: DrawerBindableInput,
|
||||||
|
props: (opts: FieldProps = {} as FieldProps) => {
|
||||||
|
const { key, field } = opts
|
||||||
|
return {
|
||||||
|
title: field.title ?? getFieldLabel(key, field),
|
||||||
|
panel: AutomationBindingPanel,
|
||||||
|
type: field.customType,
|
||||||
|
updateOnChange: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SchemaFieldTypes.QUERY_LIMIT]: {
|
||||||
|
comp: DrawerBindableInput,
|
||||||
|
props: (opts: FieldProps = {} as FieldProps) => {
|
||||||
|
const { key, field } = opts
|
||||||
|
return {
|
||||||
|
title: field.title ?? getFieldLabel(key, field),
|
||||||
|
panel: AutomationBindingPanel,
|
||||||
|
type: field.customType,
|
||||||
|
updateOnChange: false,
|
||||||
|
placeholder: queryLimit,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Not a core schema type.
|
||||||
|
[SchemaFieldTypes.FIELDS]: {
|
||||||
|
comp: FieldSelector,
|
||||||
|
props: () => {
|
||||||
|
return {
|
||||||
|
isTestModal: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fullWidth: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
$: isTrigger = block?.type === AutomationStepType.TRIGGER
|
||||||
|
|
||||||
|
// The step input properties
|
||||||
|
$: inputData = automationStore.actions.getInputData(block)
|
||||||
|
|
||||||
|
// Automation definition properties
|
||||||
|
$: schemaProperties = Object.entries(block?.schema?.inputs?.properties || {})
|
||||||
|
|
||||||
|
// Used for field labelling
|
||||||
|
$: requiredProperties = block ? block.schema["inputs"].required : []
|
||||||
|
|
||||||
|
$: tableId = inputData && "tableId" in inputData ? inputData.tableId : null
|
||||||
|
$: table = tableId
|
||||||
|
? $tables.list.find(table => table._id === tableId)
|
||||||
|
: { schema: {} }
|
||||||
|
|
||||||
|
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||||
|
|
||||||
|
const canShowField = (value: BaseIOStructure, inputData?: StepInputs) => {
|
||||||
|
if (!inputData) {
|
||||||
|
console.error("Cannot determine field visibility without field data")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const dependsOn = value?.dependsOn
|
||||||
|
return !dependsOn || !!getInputValue(inputData, dependsOn)
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultChange = (
|
||||||
|
update: FormUpdate,
|
||||||
|
block?: AutomationTrigger | AutomationStep
|
||||||
|
) => {
|
||||||
|
if (block) {
|
||||||
|
automationStore.actions.requestUpdate(update, block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Build the core props used to page the Automation Schema field.
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @param field
|
||||||
|
* @param block
|
||||||
|
*/
|
||||||
|
const schemaToUI = (
|
||||||
|
key: string,
|
||||||
|
field: BaseIOStructure,
|
||||||
|
block?: AutomationStep | AutomationTrigger
|
||||||
|
) => {
|
||||||
|
if (!block) {
|
||||||
|
console.error("Could not process UI elements.")
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
const type = getFieldType(field, block)
|
||||||
|
const config = type ? SchemaTypes[type] : null
|
||||||
|
const title = getFieldLabel(key, field, requiredProperties?.includes(key))
|
||||||
|
const value = getInputValue(inputData, key)
|
||||||
|
const meta = getInputValue(inputData, "meta")
|
||||||
|
|
||||||
|
return {
|
||||||
|
meta,
|
||||||
|
config,
|
||||||
|
value,
|
||||||
|
title,
|
||||||
|
props: config?.props
|
||||||
|
? config.props({ key, field, block, value, meta, title })
|
||||||
|
: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the correct UI element based on the automation
|
||||||
|
* schema configuration.
|
||||||
|
*
|
||||||
|
* @param field
|
||||||
|
* @param block
|
||||||
|
*/
|
||||||
|
const getFieldType = (
|
||||||
|
field: BaseIOStructure,
|
||||||
|
block?: AutomationStep | AutomationTrigger
|
||||||
|
) => {
|
||||||
|
// Direct customType map
|
||||||
|
const customType = field.customType && customTypeToSchema[field.customType]
|
||||||
|
if (customType) {
|
||||||
|
return customType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct type map
|
||||||
|
const fieldType = field.type && typeToSchema[field.type]
|
||||||
|
if (fieldType) {
|
||||||
|
return fieldType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enum Field
|
||||||
|
if (field.type === AutomationIOType.STRING && field.enum) {
|
||||||
|
return SchemaFieldTypes.ENUM
|
||||||
|
}
|
||||||
|
|
||||||
|
// JS V2
|
||||||
|
if (
|
||||||
|
field.customType === AutomationCustomIOType.CODE &&
|
||||||
|
block?.stepId === AutomationActionStepId.EXECUTE_SCRIPT_V2
|
||||||
|
) {
|
||||||
|
return SchemaFieldTypes.CODE_V2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter step or trigger
|
||||||
|
if (
|
||||||
|
field.customType === AutomationCustomIOType.FILTERS ||
|
||||||
|
field.customType === AutomationCustomIOType.TRIGGER_FILTER
|
||||||
|
) {
|
||||||
|
return SchemaFieldTypes.FILTER
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default string/number UX
|
||||||
|
if (
|
||||||
|
field.type === AutomationIOType.STRING ||
|
||||||
|
field.type === AutomationIOType.NUMBER
|
||||||
|
) {
|
||||||
|
// "integer" removed as it isnt present in the type
|
||||||
|
return SchemaFieldTypes.STRING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each schemaProperties as [key, field]}
|
||||||
|
{#if canShowField(field, inputData)}
|
||||||
|
{@const { config, props, value, title } = schemaToUI(key, field, block)}
|
||||||
|
{#if config}
|
||||||
|
{#if config.wrapped === false}
|
||||||
|
<svelte:component
|
||||||
|
this={config.comp}
|
||||||
|
{bindings}
|
||||||
|
{block}
|
||||||
|
{context}
|
||||||
|
{key}
|
||||||
|
{...{ value, ...props }}
|
||||||
|
on:change={config.onChange ??
|
||||||
|
(e => {
|
||||||
|
defaultChange({ [key]: e.detail }, block)
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<PropField label={title} fullWidth labelTooltip={config.tooltip || ""}>
|
||||||
|
<svelte:component
|
||||||
|
this={config.comp}
|
||||||
|
{bindings}
|
||||||
|
{block}
|
||||||
|
{context}
|
||||||
|
{key}
|
||||||
|
{...{ value, ...props }}
|
||||||
|
on:change={config.onChange ??
|
||||||
|
(e => {
|
||||||
|
defaultChange({ [key]: e.detail }, block)
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</PropField>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
|
@ -1,15 +1,18 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label } from "@budibase/bbui"
|
import { Select } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { automationStore, selectedAutomation } from "@/stores/builder"
|
import { automationStore, selectedAutomation } from "@/stores/builder"
|
||||||
import { TriggerStepID } from "@/constants/backend/automations"
|
import { TriggerStepID } from "@/constants/backend/automations"
|
||||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
|
import PropField from "./PropField.svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
export let title
|
||||||
|
|
||||||
const onChangeAutomation = e => {
|
const onChangeAutomation = e => {
|
||||||
value.automationId = e.detail
|
value.automationId = e.detail
|
||||||
dispatch("change", value)
|
dispatch("change", value)
|
||||||
|
@ -33,9 +36,8 @@
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="schema-field">
|
<div class="selector">
|
||||||
<Label>Automation</Label>
|
<PropField label={title} fullWidth>
|
||||||
<div class="field-width">
|
|
||||||
<Select
|
<Select
|
||||||
on:change={onChangeAutomation}
|
on:change={onChangeAutomation}
|
||||||
value={value.automationId}
|
value={value.automationId}
|
||||||
|
@ -43,13 +45,12 @@
|
||||||
getOptionValue={automation => automation._id}
|
getOptionValue={automation => automation._id}
|
||||||
getOptionLabel={automation => automation.name}
|
getOptionLabel={automation => automation.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</PropField>
|
||||||
</div>
|
</div>
|
||||||
{#if Object.keys(automationFields)}
|
{#if Object.keys(automationFields)}
|
||||||
{#each Object.keys(automationFields) as field}
|
{#each Object.keys(automationFields) as field}
|
||||||
<div class="schema-field">
|
<div class="schema-field">
|
||||||
<Label>{field}</Label>
|
<PropField label={field} fullWidth>
|
||||||
<div class="field-width">
|
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
extraThin
|
extraThin
|
||||||
|
@ -59,27 +60,12 @@
|
||||||
{bindings}
|
{bindings}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</PropField>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.field-width {
|
|
||||||
width: 320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schema-field {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
flex: 1;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schema-field :global(label) {
|
.schema-field :global(label) {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,385 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Divider,
|
||||||
|
ActionButton,
|
||||||
|
notifications,
|
||||||
|
Helpers,
|
||||||
|
Icon,
|
||||||
|
Button,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import JSONViewer, {
|
||||||
|
type JSONViewerClickEvent,
|
||||||
|
} from "@/components/common/JSONViewer.svelte"
|
||||||
|
import {
|
||||||
|
type AutomationStep,
|
||||||
|
type AutomationTrigger,
|
||||||
|
type Automation,
|
||||||
|
type LoopStepInputs,
|
||||||
|
type DidNotTriggerResponse,
|
||||||
|
type TestAutomationResponse,
|
||||||
|
type BlockRef,
|
||||||
|
type AutomationIOProps,
|
||||||
|
type AutomationStepResult,
|
||||||
|
AutomationStepStatus,
|
||||||
|
AutomationStatus,
|
||||||
|
AutomationStepType,
|
||||||
|
isDidNotTriggerResponse,
|
||||||
|
isTrigger,
|
||||||
|
isFilterStep,
|
||||||
|
isLoopStep,
|
||||||
|
BlockDefinitionTypes,
|
||||||
|
AutomationActionStepId,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import Count from "./Count.svelte"
|
||||||
|
import { automationStore, selectedAutomation } from "@/stores/builder"
|
||||||
|
import {
|
||||||
|
type BlockStatus,
|
||||||
|
BlockStatusSource,
|
||||||
|
BlockStatusType,
|
||||||
|
DataMode,
|
||||||
|
RowSteps,
|
||||||
|
} from "@/types/automations"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { cloneDeep } from "lodash"
|
||||||
|
import { type AutomationContext } from "@/stores/builder/automations"
|
||||||
|
|
||||||
|
export let context: AutomationContext | undefined
|
||||||
|
export let block: AutomationStep | AutomationTrigger | undefined
|
||||||
|
export let automation: Automation | undefined
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const DataModeTabs = {
|
||||||
|
[DataMode.INPUT]: "Data In",
|
||||||
|
[DataMode.OUTPUT]: "Data Out",
|
||||||
|
[DataMode.ERRORS]: "Errors",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add explicit weight to the issue types
|
||||||
|
const issueOrder = {
|
||||||
|
[BlockStatusType.ERROR]: 0,
|
||||||
|
[BlockStatusType.WARN]: 1,
|
||||||
|
[BlockStatusType.INFO]: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataMode: DataMode = DataMode.INPUT
|
||||||
|
let issues: BlockStatus[] = []
|
||||||
|
|
||||||
|
$: blockRef = block?.id
|
||||||
|
? $selectedAutomation?.blockRefs?.[block.id]
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
// The loop block associated with the selected block
|
||||||
|
$: loopRef = blockRef?.looped
|
||||||
|
? $selectedAutomation.blockRefs[blockRef.looped]
|
||||||
|
: undefined
|
||||||
|
$: loopBlock = automationStore.actions.getBlockByRef(automation, loopRef)
|
||||||
|
|
||||||
|
$: if ($automationStore.selectedNodeId) {
|
||||||
|
dataMode = $automationStore.selectedNodeMode || DataMode.INPUT
|
||||||
|
}
|
||||||
|
|
||||||
|
$: testResults = $automationStore.testResults as TestAutomationResponse
|
||||||
|
$: blockResults = automationStore.actions.processBlockResults(
|
||||||
|
testResults,
|
||||||
|
block
|
||||||
|
)
|
||||||
|
$: processTestIssues(testResults, block)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take the results of an automation and generate a
|
||||||
|
* list of graded issues. If the conditions get bigger they
|
||||||
|
* could be broken out.
|
||||||
|
*
|
||||||
|
* @param testResults
|
||||||
|
* @param block
|
||||||
|
*/
|
||||||
|
const processTestIssues = (
|
||||||
|
testResults: TestAutomationResponse,
|
||||||
|
block: AutomationStep | AutomationTrigger | undefined
|
||||||
|
) => {
|
||||||
|
// Reset issues
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
if (!testResults || !block || !blockResults) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process loop issues
|
||||||
|
if (blockRef?.looped && loopBlock && isLoopStep(loopBlock)) {
|
||||||
|
const inputs = loopBlock.inputs as LoopStepInputs
|
||||||
|
const loopMax = Number(inputs.iterations)
|
||||||
|
const loopOutputs = blockResults?.outputs
|
||||||
|
|
||||||
|
let loopMessage = "There was an error"
|
||||||
|
|
||||||
|
// Not technically failed as it continues to run
|
||||||
|
if (loopOutputs?.status === AutomationStepStatus.MAX_ITERATIONS) {
|
||||||
|
loopMessage = `The maximum number of iterations (${loopMax}) has been reached.`
|
||||||
|
} else if (
|
||||||
|
loopOutputs?.status === AutomationStepStatus.FAILURE_CONDITION
|
||||||
|
) {
|
||||||
|
loopMessage = `The failure condition for the loop was hit: ${inputs.failure}.
|
||||||
|
The loop was terminated`
|
||||||
|
} else if (loopOutputs?.status === AutomationStepStatus.INCORRECT_TYPE) {
|
||||||
|
loopMessage = `An 'Input Type' of '${inputs.option}' was configured which does
|
||||||
|
not match the value supplied`
|
||||||
|
}
|
||||||
|
|
||||||
|
issues.push({
|
||||||
|
message: loopMessage,
|
||||||
|
type: BlockStatusType.ERROR,
|
||||||
|
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process filtered row issues
|
||||||
|
else if (isTrigger(block) && isDidNotTriggerResponse(testResults)) {
|
||||||
|
issues.push({
|
||||||
|
message: (blockResults as DidNotTriggerResponse).message,
|
||||||
|
type: BlockStatusType.WARN,
|
||||||
|
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter step
|
||||||
|
else if (
|
||||||
|
isFilterStep(block) &&
|
||||||
|
blockResults?.outputs.status === AutomationStatus.STOPPED
|
||||||
|
) {
|
||||||
|
issues.push({
|
||||||
|
message: `The conditions were not met and
|
||||||
|
the automation flow was stopped.`,
|
||||||
|
type: BlockStatusType.WARN,
|
||||||
|
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row step issues
|
||||||
|
else if (!isTrigger(block) && RowSteps.includes(block.stepId)) {
|
||||||
|
const outputs = (blockResults as AutomationStepResult)?.outputs
|
||||||
|
if (outputs.success) return
|
||||||
|
issues.push({
|
||||||
|
message: `Could not complete the row request:
|
||||||
|
${outputs.response?.message || JSON.stringify(outputs.response)}`,
|
||||||
|
type: BlockStatusType.ERROR,
|
||||||
|
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default on error.
|
||||||
|
else if (!isTrigger(block) && !blockResults.outputs.success) {
|
||||||
|
issues.push({
|
||||||
|
message: `There was an issue with the step. See 'Data out' for more information`,
|
||||||
|
type: BlockStatusType.ERROR,
|
||||||
|
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the issues
|
||||||
|
issues.sort((a, b) => issueOrder[a.type] - issueOrder[b.type])
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyContext = (e: JSONViewerClickEvent) => {
|
||||||
|
Helpers.copyToClipboard(JSON.stringify(e.detail?.value))
|
||||||
|
notifications.success("Copied to clipboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take the core AutomationContext and humanise it
|
||||||
|
* Before the test is run, populate the "Data in" values
|
||||||
|
* with the schema definition keyed by the readable step names.
|
||||||
|
*/
|
||||||
|
const parseContext = (context?: AutomationContext, blockRef?: BlockRef) => {
|
||||||
|
if (!blockRef || !automation) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let clonetext = cloneDeep(context)
|
||||||
|
|
||||||
|
const pathSteps = automationStore.actions.getPathSteps(
|
||||||
|
blockRef.pathTo,
|
||||||
|
automation
|
||||||
|
)
|
||||||
|
|
||||||
|
const defs = $automationStore.blockDefinitions
|
||||||
|
const stepNames = automation.definition.stepNames
|
||||||
|
const loopDef =
|
||||||
|
defs[BlockDefinitionTypes.ACTION]?.[AutomationActionStepId.LOOP]
|
||||||
|
|
||||||
|
// Exclude the trigger and loops from steps
|
||||||
|
const filteredSteps: Record<string, AutomationStep | AutomationIOProps> =
|
||||||
|
pathSteps
|
||||||
|
.slice(0, -1)
|
||||||
|
.filter(
|
||||||
|
(step): step is AutomationStep =>
|
||||||
|
step.type === AutomationStepType.ACTION &&
|
||||||
|
step.stepId !== AutomationActionStepId.LOOP
|
||||||
|
)
|
||||||
|
.reduce(
|
||||||
|
(
|
||||||
|
acc: Record<string, AutomationStep | AutomationIOProps>,
|
||||||
|
step: AutomationStep
|
||||||
|
) => {
|
||||||
|
// Process the block
|
||||||
|
const blockRef = $selectedAutomation.blockRefs[step.id]
|
||||||
|
const blockDefinition =
|
||||||
|
defs[BlockDefinitionTypes.ACTION]?.[step.stepId]
|
||||||
|
|
||||||
|
// Check if the block has a loop
|
||||||
|
const loopRef = blockRef.looped
|
||||||
|
? $selectedAutomation.blockRefs[blockRef.looped]
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const testData = context?.steps?.[step.id]
|
||||||
|
const sampleDef = loopRef ? loopDef : blockDefinition
|
||||||
|
|
||||||
|
// Prioritise the display of testData and fallback to step defs
|
||||||
|
// This will ensure users have at least an idea of what they are looking at
|
||||||
|
acc[stepNames?.[step.id] || step.name] =
|
||||||
|
testData || sampleDef?.schema.outputs.properties || {}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, AutomationStep | AutomationIOProps>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { ...clonetext, steps: filteredSteps }
|
||||||
|
}
|
||||||
|
|
||||||
|
$: parsedContext = parseContext(context, blockRef)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
{#each Object.values(DataMode) as mode}
|
||||||
|
<Count count={mode === DataMode.ERRORS ? issues.length : 0}>
|
||||||
|
<ActionButton
|
||||||
|
selected={mode === dataMode}
|
||||||
|
quiet
|
||||||
|
on:click={() => {
|
||||||
|
dataMode = mode
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{DataModeTabs[mode]}
|
||||||
|
</ActionButton>
|
||||||
|
</Count>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<Divider noMargin />
|
||||||
|
<div class="viewer">
|
||||||
|
{#if dataMode === DataMode.INPUT}
|
||||||
|
<JSONViewer
|
||||||
|
value={parsedContext}
|
||||||
|
showCopyIcon
|
||||||
|
on:click-copy={copyContext}
|
||||||
|
/>
|
||||||
|
{:else if dataMode === DataMode.OUTPUT}
|
||||||
|
{#if blockResults}
|
||||||
|
<JSONViewer
|
||||||
|
value={blockResults.outputs}
|
||||||
|
showCopyIcon
|
||||||
|
on:click-copy={copyContext}
|
||||||
|
/>
|
||||||
|
{:else if testResults && !blockResults}
|
||||||
|
<div class="content">
|
||||||
|
<span class="info">
|
||||||
|
This step was not executed as part of the test run
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="content">
|
||||||
|
<span class="info">
|
||||||
|
Run the automation to show the output of this step
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
size={"S"}
|
||||||
|
icon={"Play"}
|
||||||
|
secondary
|
||||||
|
on:click={() => {
|
||||||
|
dispatch("run")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Run
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="issues" class:empty={!issues.length}>
|
||||||
|
{#if issues.length === 0}
|
||||||
|
<span>There are no current issues</span>
|
||||||
|
{:else}
|
||||||
|
{#each issues as issue}
|
||||||
|
<div class={`issue ${issue.type}`}>
|
||||||
|
<div class="icon"><Icon name="Alert" /></div>
|
||||||
|
<!-- For custom automations, the error message needs a default -->
|
||||||
|
<div class="message">
|
||||||
|
{issue.message || "There was an error"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider noMargin />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.content .info {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
.tabs,
|
||||||
|
.viewer {
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.viewer {
|
||||||
|
overflow-y: scroll;
|
||||||
|
flex: 1;
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
.viewer .content {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
padding: var(--spacing-m) var(--spacing-l);
|
||||||
|
}
|
||||||
|
.issue {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-bottom: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.issues {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.issues.empty {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.issue.error .icon {
|
||||||
|
color: var(--spectrum-global-color-static-red-600);
|
||||||
|
}
|
||||||
|
.issue.warn .icon {
|
||||||
|
color: var(--spectrum-global-color-static-yellow-600);
|
||||||
|
}
|
||||||
|
.issues :global(hr.spectrum-Divider:last-child) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.issues .issue:not(:first-child) {
|
||||||
|
padding-top: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.issues {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,236 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Body, Tags, Tag, Icon, TooltipPosition } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
type AutomationStep,
|
||||||
|
type AutomationTrigger,
|
||||||
|
type Automation,
|
||||||
|
AutomationStepType,
|
||||||
|
isBranchStep,
|
||||||
|
isActionStep,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import {
|
||||||
|
type ExternalAction,
|
||||||
|
externalActions,
|
||||||
|
} from "@/components/automation/AutomationBuilder/FlowChart/ExternalActions"
|
||||||
|
import { automationStore } from "@/stores/builder"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let block: AutomationStep | AutomationTrigger | undefined = undefined
|
||||||
|
export let automation: Automation | undefined = undefined
|
||||||
|
export let itemName: string | undefined = undefined
|
||||||
|
export let disabled: boolean = false
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let externalAction: ExternalAction | undefined
|
||||||
|
let editing: boolean
|
||||||
|
let validRegex: RegExp = /^[A-Za-z0-9_\s]+$/
|
||||||
|
|
||||||
|
$: stepNames = automation?.definition.stepNames || {}
|
||||||
|
$: blockHeading = getHeading(itemName, block) || ""
|
||||||
|
$: blockNameError = getStepNameError(blockHeading)
|
||||||
|
$: isBranch = block && isBranchStep(block)
|
||||||
|
|
||||||
|
const getHeading = (
|
||||||
|
itemName?: string,
|
||||||
|
block?: AutomationStep | AutomationTrigger
|
||||||
|
) => {
|
||||||
|
if (itemName) {
|
||||||
|
return itemName
|
||||||
|
} else if (block) {
|
||||||
|
return stepNames?.[block?.id] || block?.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: isTrigger = block?.type === AutomationStepType.TRIGGER
|
||||||
|
$: allSteps = automation?.definition.steps || []
|
||||||
|
$: blockDefinition = automationStore.actions.getBlockDefinition(block)
|
||||||
|
|
||||||
|
// Put the type in the header if they change the name
|
||||||
|
// Otherwise the info is obscured
|
||||||
|
$: blockTitle = isBranch
|
||||||
|
? "Branch"
|
||||||
|
: `${
|
||||||
|
block &&
|
||||||
|
block?.id in stepNames &&
|
||||||
|
stepNames[block?.id] !== blockDefinition?.name
|
||||||
|
? blockDefinition?.name
|
||||||
|
: "Step"
|
||||||
|
}`
|
||||||
|
|
||||||
|
// Parse external actions
|
||||||
|
$: if (block && isActionStep(block) && block?.stepId in externalActions) {
|
||||||
|
externalAction = externalActions[block?.stepId]
|
||||||
|
} else {
|
||||||
|
externalAction = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStepNameError = (name: string) => {
|
||||||
|
if (!block) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const duplicateError =
|
||||||
|
"This name already exists, please enter a unique name"
|
||||||
|
if (editing) {
|
||||||
|
for (const [key, value] of Object.entries(stepNames)) {
|
||||||
|
if (name !== block.name && name === value && key !== block.id) {
|
||||||
|
return duplicateError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const step of allSteps) {
|
||||||
|
if (step.id !== block.id && name === step.name) {
|
||||||
|
return duplicateError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name !== block.name && name?.length > 0) {
|
||||||
|
let invalidRoleName = !validRegex.test(name)
|
||||||
|
if (invalidRoleName) {
|
||||||
|
return "Please enter a name consisting of only alphanumeric symbols and underscores"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startEditing = () => {
|
||||||
|
editing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopEditing = () => {
|
||||||
|
editing = false
|
||||||
|
if (blockNameError) {
|
||||||
|
blockHeading =
|
||||||
|
(block ? stepNames[block?.id] : undefined) || block?.name || ""
|
||||||
|
} else {
|
||||||
|
dispatch("update", blockHeading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInput = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
blockHeading = target.value.trim()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="block-details">
|
||||||
|
{#if block}
|
||||||
|
{#if externalAction}
|
||||||
|
<img
|
||||||
|
alt={externalAction.name}
|
||||||
|
src={externalAction.icon}
|
||||||
|
width="28px"
|
||||||
|
height="28px"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
width="28px"
|
||||||
|
height="28px"
|
||||||
|
class="spectrum-Icon"
|
||||||
|
style="color:var(--spectrum-global-color-gray-700);"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-{block.icon}" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
<div class="heading">
|
||||||
|
{#if isTrigger}
|
||||||
|
<Body size="XS"><b>Trigger</b></Body>
|
||||||
|
{:else}
|
||||||
|
<Body size="XS">
|
||||||
|
<div class="step">
|
||||||
|
<b>{blockTitle}</b>
|
||||||
|
{#if blockDefinition?.deprecated}
|
||||||
|
<Tags>
|
||||||
|
<Tag invalid>Deprecated</Tag>
|
||||||
|
</Tags>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Body>
|
||||||
|
{/if}
|
||||||
|
<input
|
||||||
|
class="input-text"
|
||||||
|
placeholder={`Enter step name`}
|
||||||
|
name="name"
|
||||||
|
autocomplete="off"
|
||||||
|
disabled={isTrigger || disabled}
|
||||||
|
value={blockHeading}
|
||||||
|
on:input={handleInput}
|
||||||
|
on:click={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
startEditing()
|
||||||
|
}}
|
||||||
|
on:keydown={async e => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
stopEditing()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:blur={stopEditing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if blockNameError && editing}
|
||||||
|
<div class="error-container">
|
||||||
|
<div class="error-icon">
|
||||||
|
<Icon
|
||||||
|
size="S"
|
||||||
|
name="Alert"
|
||||||
|
tooltip={blockNameError}
|
||||||
|
tooltipPosition={TooltipPosition.Left}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.step {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
color: var(--ink);
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-text {
|
||||||
|
font-size: var(--spectrum-alias-font-size-default);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding-left: 0px;
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide arrows for number fields */
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon :global(.spectrum-Icon) {
|
||||||
|
fill: var(--spectrum-global-color-red-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,65 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { automationStore } from "@/stores/builder"
|
||||||
|
import { memo } from "@budibase/frontend-core"
|
||||||
|
import { environment } from "@/stores/portal"
|
||||||
|
import {
|
||||||
|
type AutomationStep,
|
||||||
|
type AutomationTrigger,
|
||||||
|
type Automation,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { type SchemaConfigProps } from "@/types/automations"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { getCustomStepLayout } from "./layouts"
|
||||||
|
import AutomationSchemaLayout from "./AutomationSchemaLayout.svelte"
|
||||||
|
import AutomationCustomLayout from "./AutomationCustomLayout.svelte"
|
||||||
|
|
||||||
|
export let block: AutomationStep | AutomationTrigger | undefined = undefined
|
||||||
|
export let automation: Automation | undefined = undefined
|
||||||
|
export let context: {} | undefined
|
||||||
|
|
||||||
|
const memoEnvVariables = memo($environment.variables)
|
||||||
|
|
||||||
|
// Databindings requires TypeScript conversion
|
||||||
|
let environmentBindings: any[]
|
||||||
|
|
||||||
|
// All bindings available to this point
|
||||||
|
$: availableBindings = automationStore.actions.getPathBindings(
|
||||||
|
block?.id,
|
||||||
|
automation
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fetch the env bindings
|
||||||
|
$: if ($memoEnvVariables) {
|
||||||
|
environmentBindings = automationStore.actions.buildEnvironmentBindings()
|
||||||
|
}
|
||||||
|
$: userBindings = automationStore.actions.buildUserBindings()
|
||||||
|
$: settingBindings = automationStore.actions.buildSettingBindings()
|
||||||
|
|
||||||
|
// Combine all bindings for the step
|
||||||
|
$: bindings = [
|
||||||
|
...availableBindings,
|
||||||
|
...environmentBindings,
|
||||||
|
...userBindings,
|
||||||
|
...settingBindings,
|
||||||
|
]
|
||||||
|
|
||||||
|
// Store for any UX related data
|
||||||
|
const stepStore = writable<Record<string, any>>({})
|
||||||
|
$: if (block?.id) {
|
||||||
|
stepStore.update(state => ({ ...state, [block.id]: {} }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if any custom step layouts have been created
|
||||||
|
let customLayout: SchemaConfigProps[] | undefined
|
||||||
|
$: if ($stepStore) {
|
||||||
|
customLayout = getCustomStepLayout(block, stepStore)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if customLayout}
|
||||||
|
<!-- Render custom layout 1 or more components in a custom layout -->
|
||||||
|
<AutomationCustomLayout {context} {bindings} {block} layout={customLayout} />
|
||||||
|
{:else}
|
||||||
|
<!-- Render Automation Step Schema > [string, BaseIOStructure][] -->
|
||||||
|
<AutomationSchemaLayout {context} {bindings} {block} />
|
||||||
|
{/if}
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { AbsTooltip } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let count: number = 0
|
||||||
|
export let tooltip: string | undefined = undefined
|
||||||
|
export let hoverable: boolean = true
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<AbsTooltip text={tooltip}>
|
||||||
|
{#if count}
|
||||||
|
<span
|
||||||
|
class="count"
|
||||||
|
role="button"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-label={`Notifications ${count}`}
|
||||||
|
class:hoverable
|
||||||
|
on:mouseenter={() => {
|
||||||
|
dispatch("hover")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</AbsTooltip>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.count {
|
||||||
|
position: absolute;
|
||||||
|
right: -6px;
|
||||||
|
top: -6px;
|
||||||
|
background: var(--spectrum-global-color-static-red-600);
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 4px;
|
||||||
|
z-index: 2;
|
||||||
|
font-size: 0.8em;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.count.hoverable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,37 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
DrawerBindableSlot,
|
||||||
|
ServerBindingPanel as AutomationBindingPanel,
|
||||||
|
} from "@/components/common/bindings"
|
||||||
|
import { DatePicker } from "@budibase/bbui"
|
||||||
|
import { type EnrichedBinding } from "@budibase/types"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let context: Record<any, any> | undefined = undefined
|
||||||
|
export let bindings: EnrichedBinding[] | undefined = undefined
|
||||||
|
export let value: string | undefined = undefined
|
||||||
|
export let title: string | undefined = undefined
|
||||||
|
export let disabled: boolean | undefined = false
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DrawerBindableSlot
|
||||||
|
{title}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
type={"date"}
|
||||||
|
{value}
|
||||||
|
on:change={e => dispatch("change", e.detail)}
|
||||||
|
{bindings}
|
||||||
|
allowJS={true}
|
||||||
|
updateOnChange={false}
|
||||||
|
{disabled}
|
||||||
|
{context}
|
||||||
|
>
|
||||||
|
<DatePicker
|
||||||
|
{value}
|
||||||
|
on:change={e => {
|
||||||
|
dispatch("change", e.detail)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DrawerBindableSlot>
|
|
@ -0,0 +1,111 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { BindingSidePanel } from "@/components/common/bindings"
|
||||||
|
import {
|
||||||
|
BindingType,
|
||||||
|
BindingHelpers,
|
||||||
|
} from "@/components/common/bindings/utils"
|
||||||
|
import CodeEditor from "@/components/common/CodeEditor/CodeEditor.svelte"
|
||||||
|
import {
|
||||||
|
bindingsToCompletions,
|
||||||
|
hbAutocomplete,
|
||||||
|
EditorModes,
|
||||||
|
} from "@/components/common/CodeEditor"
|
||||||
|
import { CodeEditorModal } from "@/components/automation/SetupPanel"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import {
|
||||||
|
AutomationActionStepId,
|
||||||
|
type AutomationStep,
|
||||||
|
type CaretPositionFn,
|
||||||
|
type InsertAtPositionFn,
|
||||||
|
type EnrichedBinding,
|
||||||
|
BindingMode,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
export let value: string | undefined
|
||||||
|
export let isJS: boolean = true
|
||||||
|
export let block: AutomationStep
|
||||||
|
export let context
|
||||||
|
export let bindings
|
||||||
|
|
||||||
|
let getCaretPosition: CaretPositionFn | undefined
|
||||||
|
let insertAtPos: InsertAtPositionFn | undefined
|
||||||
|
|
||||||
|
$: codeMode =
|
||||||
|
block.stepId === AutomationActionStepId.EXECUTE_BASH
|
||||||
|
? EditorModes.Handlebars
|
||||||
|
: EditorModes.JS
|
||||||
|
$: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, {
|
||||||
|
disableWrapping: true,
|
||||||
|
})
|
||||||
|
$: editingJs = codeMode === EditorModes.JS
|
||||||
|
$: stepCompletions =
|
||||||
|
codeMode === EditorModes.Handlebars
|
||||||
|
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||||
|
: []
|
||||||
|
|
||||||
|
const addBinding = (binding: EnrichedBinding) =>
|
||||||
|
bindingsHelpers.onSelectBinding(value, binding, {
|
||||||
|
js: true,
|
||||||
|
dontDecode: true,
|
||||||
|
type: BindingType.RUNTIME,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- DEPRECATED -->
|
||||||
|
<CodeEditorModal
|
||||||
|
on:hide={() => {
|
||||||
|
// Push any pending changes when the window closes
|
||||||
|
dispatch("change", value)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class:js-editor={isJS}>
|
||||||
|
<div class:js-code={isJS} style="width:100%;height:500px;">
|
||||||
|
<CodeEditor
|
||||||
|
{value}
|
||||||
|
on:change={e => {
|
||||||
|
// need to pass without the value inside
|
||||||
|
value = e.detail
|
||||||
|
}}
|
||||||
|
completions={stepCompletions}
|
||||||
|
mode={codeMode}
|
||||||
|
autocompleteEnabled={codeMode !== EditorModes.JS}
|
||||||
|
bind:getCaretPosition
|
||||||
|
bind:insertAtPos
|
||||||
|
placeholder={codeMode === EditorModes.Handlebars
|
||||||
|
? "Add bindings by typing {{"
|
||||||
|
: null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if editingJs}
|
||||||
|
<div class="js-binding-picker">
|
||||||
|
<BindingSidePanel
|
||||||
|
{bindings}
|
||||||
|
allowHelpers={false}
|
||||||
|
{addBinding}
|
||||||
|
mode={BindingMode.JavaScript}
|
||||||
|
{context}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</CodeEditorModal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.js-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.js-code {
|
||||||
|
flex: 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.js-binding-picker {
|
||||||
|
flex: 3;
|
||||||
|
margin-top: calc((var(--spacing-xl) * -1) + 1px);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,67 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
DrawerBindableSlot,
|
||||||
|
ServerBindingPanel as AutomationBindingPanel,
|
||||||
|
} from "@/components/common/bindings"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { type EnrichedBinding, FieldType } from "@budibase/types"
|
||||||
|
import CodeEditorField from "@/components/common/bindings/CodeEditorField.svelte"
|
||||||
|
import { DropdownPosition } from "@/components/common/CodeEditor/CodeEditor.svelte"
|
||||||
|
|
||||||
|
export let value: string
|
||||||
|
export let context: Record<any, any> | undefined = undefined
|
||||||
|
export let bindings: EnrichedBinding[] | undefined
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="scriptv2-wrapper">
|
||||||
|
<DrawerBindableSlot
|
||||||
|
title={"Edit Code"}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
type={FieldType.LONGFORM}
|
||||||
|
on:change={e => dispatch("change", e.detail)}
|
||||||
|
{value}
|
||||||
|
{bindings}
|
||||||
|
allowJS={true}
|
||||||
|
allowHBS={false}
|
||||||
|
updateOnChange={false}
|
||||||
|
{context}
|
||||||
|
>
|
||||||
|
<div class="field-wrap code-editor">
|
||||||
|
<CodeEditorField
|
||||||
|
{value}
|
||||||
|
{bindings}
|
||||||
|
{context}
|
||||||
|
placeholder={"Add bindings by typing $"}
|
||||||
|
on:change={e => dispatch("change", e.detail)}
|
||||||
|
dropdown={DropdownPosition.Relative}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DrawerBindableSlot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.field-wrap :global(.cm-editor),
|
||||||
|
.field-wrap :global(.cm-scroller) {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.field-wrap {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-400);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.field-wrap.code-editor {
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scriptv2-wrapper :global(.icon.slot-icon),
|
||||||
|
.scriptv2-wrapper :global(.text-area-slot-icon) {
|
||||||
|
right: 1px;
|
||||||
|
top: 1px;
|
||||||
|
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
|
||||||
|
border-bottom-left-radius: var(--spectrum-alias-border-radius-regular);
|
||||||
|
border-right: 0px;
|
||||||
|
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,89 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import KeyValueBuilder from "@/components/integration/KeyValueBuilder.svelte"
|
||||||
|
import { Toggle } from "@budibase/bbui"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import {
|
||||||
|
DrawerBindableInput,
|
||||||
|
ServerBindingPanel as AutomationBindingPanel,
|
||||||
|
} from "@/components/common/bindings"
|
||||||
|
import { type KeyValuePair } from "@/types/automations"
|
||||||
|
|
||||||
|
export let useAttachmentBinding
|
||||||
|
export let buttonText
|
||||||
|
export let key
|
||||||
|
export let context
|
||||||
|
export let value
|
||||||
|
export let bindings
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
interface Attachment {
|
||||||
|
filename: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAttachmentParams = (keyValueObj: Attachment[]) => {
|
||||||
|
let params: Record<string, string> = {}
|
||||||
|
if (keyValueObj?.length) {
|
||||||
|
for (let param of keyValueObj) {
|
||||||
|
params[param.url] = param.filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyValueChange = (e: CustomEvent<KeyValuePair[]>) => {
|
||||||
|
const update = {
|
||||||
|
[key]: e.detail.map(({ name, value }) => ({
|
||||||
|
url: name,
|
||||||
|
filename: value,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch("change", update)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="attachment-field-wrapper">
|
||||||
|
<div class="toggle-container">
|
||||||
|
<Toggle
|
||||||
|
value={useAttachmentBinding}
|
||||||
|
text={"Use bindings"}
|
||||||
|
on:change={e => {
|
||||||
|
dispatch("change", {
|
||||||
|
[key]: null,
|
||||||
|
meta: {
|
||||||
|
useAttachmentBinding: e.detail,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="attachment-field-width">
|
||||||
|
{#if !useAttachmentBinding}
|
||||||
|
{#key value}
|
||||||
|
<KeyValueBuilder
|
||||||
|
on:change={handleKeyValueChange}
|
||||||
|
object={handleAttachmentParams(value)}
|
||||||
|
allowJS
|
||||||
|
{bindings}
|
||||||
|
keyBindings
|
||||||
|
customButtonText={buttonText}
|
||||||
|
keyPlaceholder={"URL"}
|
||||||
|
valuePlaceholder={"Filename"}
|
||||||
|
{context}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
{:else}
|
||||||
|
<DrawerBindableInput
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
{value}
|
||||||
|
on:change={e => dispatch("change", { [key]: e.detail })}
|
||||||
|
{bindings}
|
||||||
|
updateOnChange={false}
|
||||||
|
{context}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,122 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ActionButton, Drawer, DrawerContent, Button } from "@budibase/bbui"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { getSchemaForDatasourcePlus } from "@/dataBinding"
|
||||||
|
import { ServerBindingPanel as AutomationBindingPanel } from "@/components/common/bindings"
|
||||||
|
import FilterBuilder from "@/components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||||
|
import { tables, automationStore } from "@/stores/builder"
|
||||||
|
import type {
|
||||||
|
AutomationStep,
|
||||||
|
AutomationTrigger,
|
||||||
|
BaseIOStructure,
|
||||||
|
UISearchFilter,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { AutomationCustomIOType } from "@budibase/types"
|
||||||
|
import { utils } from "@budibase/shared-core"
|
||||||
|
import { QueryUtils, Utils, search } from "@budibase/frontend-core"
|
||||||
|
import { cloneDeep } from "lodash"
|
||||||
|
import { type DynamicProperties, type StepInputs } from "@/types/automations"
|
||||||
|
|
||||||
|
export let block: AutomationStep | AutomationTrigger | undefined
|
||||||
|
export let context
|
||||||
|
export let bindings
|
||||||
|
export let key
|
||||||
|
|
||||||
|
// Mix in dynamic filter props
|
||||||
|
type DynamicInputs = StepInputs & DynamicProperties
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let drawer: Drawer
|
||||||
|
|
||||||
|
$: inputData = automationStore.actions.getInputData(block)
|
||||||
|
|
||||||
|
$: schemaProperties = Object.entries(block?.schema?.inputs?.properties || {})
|
||||||
|
$: filters = lookForFilters(schemaProperties)
|
||||||
|
$: filterCount =
|
||||||
|
filters?.groups?.reduce((acc: number, group) => {
|
||||||
|
acc = acc += group?.filters?.length || 0
|
||||||
|
return acc
|
||||||
|
}, 0) || 0
|
||||||
|
|
||||||
|
$: tempFilters = cloneDeep(filters)
|
||||||
|
|
||||||
|
$: tableId = inputData && "tableId" in inputData ? inputData.tableId : null
|
||||||
|
|
||||||
|
$: schema = getSchemaForDatasourcePlus(tableId, {
|
||||||
|
searchableSchema: true,
|
||||||
|
}).schema
|
||||||
|
|
||||||
|
$: schemaFields = search.getFields(
|
||||||
|
$tables.list,
|
||||||
|
Object.values(schema || {}),
|
||||||
|
{ allowLinks: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
const lookForFilters = (
|
||||||
|
properties: [string, BaseIOStructure][]
|
||||||
|
): UISearchFilter | undefined => {
|
||||||
|
if (!properties || !inputData) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let filter: UISearchFilter | undefined
|
||||||
|
|
||||||
|
// Does not appear in the test modal. Check testData
|
||||||
|
const inputs = inputData as DynamicInputs
|
||||||
|
for (let [key, field] of properties) {
|
||||||
|
// need to look for the builder definition (keyed separately, see saveFilters)
|
||||||
|
if (
|
||||||
|
(field.customType === AutomationCustomIOType.FILTERS ||
|
||||||
|
field.customType === AutomationCustomIOType.TRIGGER_FILTER) &&
|
||||||
|
`${key}-def` in inputs
|
||||||
|
) {
|
||||||
|
filter = inputs[`${key}-def`]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(filter) ? utils.processSearchFilters(filter) : filter
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFilters(key: string, filters?: UISearchFilter) {
|
||||||
|
const update = filters ? Utils.parseFilter(filters) : filters
|
||||||
|
const query = QueryUtils.buildQuery(update)
|
||||||
|
|
||||||
|
dispatch("change", {
|
||||||
|
[key]: query,
|
||||||
|
[`${key}-def`]: update, // need to store the builder definition in the automation
|
||||||
|
})
|
||||||
|
|
||||||
|
drawer.hide()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionButton fullWidth on:click={drawer.show}>
|
||||||
|
{filterCount > 0 ? "Update Filter" : "No Filter set"}
|
||||||
|
</ActionButton>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
bind:this={drawer}
|
||||||
|
title="Filtering"
|
||||||
|
forceModal
|
||||||
|
on:drawerShow={() => {
|
||||||
|
tempFilters = filters
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button cta slot="buttons" on:click={() => saveFilters(key, tempFilters)}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DrawerContent slot="body">
|
||||||
|
<FilterBuilder
|
||||||
|
filters={tempFilters}
|
||||||
|
{bindings}
|
||||||
|
{schemaFields}
|
||||||
|
datasource={{ type: "table", tableId }}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
on:change={e => (tempFilters = e.detail)}
|
||||||
|
evaluationContext={context}
|
||||||
|
/>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
|
@ -1,10 +1,10 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { Label } from "@budibase/bbui"
|
import { Label } from "@budibase/bbui"
|
||||||
|
|
||||||
export let label
|
export let label: string | undefined = ""
|
||||||
export let labelTooltip
|
export let labelTooltip: string | undefined = ""
|
||||||
export let fullWidth = false
|
export let fullWidth: boolean | undefined = false
|
||||||
export let componentWidth = 320
|
export let componentWidth: number | undefined = 320
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { API } from "@/api"
|
||||||
|
import { tables } from "@/stores/builder"
|
||||||
|
import { type TableDatasource, SortOrder } from "@budibase/types"
|
||||||
|
import { fetchData } from "@budibase/frontend-core"
|
||||||
|
import type TableFetch from "@budibase/frontend-core/src/fetch/TableFetch"
|
||||||
|
import { Select } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let tableId: string | undefined
|
||||||
|
|
||||||
|
let datasource: TableDatasource
|
||||||
|
let fetch: TableFetch | undefined
|
||||||
|
let rowSearchTerm = ""
|
||||||
|
let selectedRow
|
||||||
|
|
||||||
|
$: primaryDisplay = table?.primaryDisplay
|
||||||
|
|
||||||
|
$: table = tableId
|
||||||
|
? $tables.list.find(table => table._id === tableId)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
$: if (table && tableId) {
|
||||||
|
datasource = { type: "table", tableId }
|
||||||
|
fetch = createFetch(datasource)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (rowSearchTerm && primaryDisplay) {
|
||||||
|
fetch?.update({
|
||||||
|
query: {
|
||||||
|
fuzzy: {
|
||||||
|
[primaryDisplay]: rowSearchTerm || "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createFetch = (datasource: TableDatasource) => {
|
||||||
|
if (!datasource || !primaryDisplay) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchData({
|
||||||
|
API,
|
||||||
|
datasource,
|
||||||
|
options: {
|
||||||
|
sortColumn: primaryDisplay,
|
||||||
|
sortOrder: SortOrder.ASCENDING,
|
||||||
|
query: {
|
||||||
|
fuzzy: {
|
||||||
|
[primaryDisplay]: rowSearchTerm || "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
limit: 20,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$: fetchedRows = fetch ? $fetch?.rows : []
|
||||||
|
$: fetchLoading = fetch ? $fetch?.loading : false
|
||||||
|
|
||||||
|
const compare = (a: any, b: any) => {
|
||||||
|
primaryDisplay && a?.[primaryDisplay] === b?.[primaryDisplay]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
placeholder={"Select a row"}
|
||||||
|
options={fetchedRows}
|
||||||
|
loading={fetchLoading}
|
||||||
|
value={selectedRow}
|
||||||
|
autocomplete={true}
|
||||||
|
getOptionLabel={row => (primaryDisplay ? row?.[primaryDisplay] : "")}
|
||||||
|
{compare}
|
||||||
|
/>
|
|
@ -11,8 +11,6 @@
|
||||||
import { FieldType } from "@budibase/types"
|
import { FieldType } from "@budibase/types"
|
||||||
|
|
||||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||||
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
|
||||||
import { FIELDS } from "@/constants/backend"
|
import { FIELDS } from "@/constants/backend"
|
||||||
import { capitalise } from "@/helpers"
|
import { capitalise } from "@/helpers"
|
||||||
import { memo } from "@budibase/frontend-core"
|
import { memo } from "@budibase/frontend-core"
|
||||||
|
@ -26,6 +24,8 @@
|
||||||
export let bindings
|
export let bindings
|
||||||
export let isTestModal
|
export let isTestModal
|
||||||
export let context = {}
|
export let context = {}
|
||||||
|
export let componentWidth
|
||||||
|
export let fullWidth = false
|
||||||
|
|
||||||
const typeToField = Object.values(FIELDS).reduce((acc, field) => {
|
const typeToField = Object.values(FIELDS).reduce((acc, field) => {
|
||||||
acc[field.type] = field
|
acc[field.type] = field
|
||||||
|
@ -234,66 +234,28 @@
|
||||||
)
|
)
|
||||||
dispatch("change", result)
|
dispatch("change", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts arrays into strings. The CodeEditor expects a string or encoded JS
|
|
||||||
* @param{object} fieldValue
|
|
||||||
*/
|
|
||||||
const drawerValue = fieldValue => {
|
|
||||||
return Array.isArray(fieldValue) ? fieldValue.join(",") : fieldValue
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each schemaFields || [] as [field, schema]}
|
{#each schemaFields || [] as [field, schema]}
|
||||||
{#if !schema.autocolumn && Object.hasOwn(editableFields, field)}
|
{#if !schema.autocolumn && Object.hasOwn(editableFields, field)}
|
||||||
<PropField label={field} fullWidth={isFullWidth(schema.type)}>
|
<PropField
|
||||||
|
label={field}
|
||||||
|
fullWidth={fullWidth || isFullWidth(schema.type)}
|
||||||
|
{componentWidth}
|
||||||
|
>
|
||||||
<div class="prop-control-wrap">
|
<div class="prop-control-wrap">
|
||||||
{#if isTestModal}
|
<RowSelectorTypes
|
||||||
<RowSelectorTypes
|
{isTestModal}
|
||||||
{isTestModal}
|
{field}
|
||||||
{field}
|
{schema}
|
||||||
{schema}
|
bindings={parsedBindings}
|
||||||
bindings={parsedBindings}
|
value={editableRow}
|
||||||
value={editableRow}
|
meta={{
|
||||||
meta={{
|
fields: editableFields,
|
||||||
fields: editableFields,
|
}}
|
||||||
}}
|
{onChange}
|
||||||
{onChange}
|
{context}
|
||||||
{context}
|
/>
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<DrawerBindableSlot
|
|
||||||
title={$memoStore?.row?.title || field}
|
|
||||||
panel={AutomationBindingPanel}
|
|
||||||
type={schema.type}
|
|
||||||
{schema}
|
|
||||||
value={drawerValue(editableRow[field])}
|
|
||||||
on:change={e =>
|
|
||||||
onChange({
|
|
||||||
row: {
|
|
||||||
[field]: e.detail,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
{bindings}
|
|
||||||
allowJS={true}
|
|
||||||
updateOnChange={false}
|
|
||||||
drawerLeft="260px"
|
|
||||||
{context}
|
|
||||||
>
|
|
||||||
<RowSelectorTypes
|
|
||||||
{isTestModal}
|
|
||||||
{field}
|
|
||||||
{schema}
|
|
||||||
bindings={parsedBindings}
|
|
||||||
value={editableRow}
|
|
||||||
meta={{
|
|
||||||
fields: editableFields,
|
|
||||||
}}
|
|
||||||
{context}
|
|
||||||
onChange={change => onChange(change)}
|
|
||||||
/>
|
|
||||||
</DrawerBindableSlot>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</PropField>
|
</PropField>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -305,24 +267,30 @@
|
||||||
class:empty={Object.is(editableFields, {})}
|
class:empty={Object.is(editableFields, {})}
|
||||||
bind:this={popoverAnchor}
|
bind:this={popoverAnchor}
|
||||||
>
|
>
|
||||||
<ActionButton
|
<PropField {componentWidth} {fullWidth}>
|
||||||
icon="Add"
|
<div class="prop-control-wrap">
|
||||||
on:click={() => {
|
<ActionButton
|
||||||
customPopover.show()
|
on:click={() => {
|
||||||
}}
|
customPopover.show()
|
||||||
disabled={!schemaFields}
|
}}
|
||||||
>Add fields
|
disabled={!schemaFields}
|
||||||
</ActionButton>
|
>
|
||||||
<ActionButton
|
Edit fields
|
||||||
icon="Remove"
|
</ActionButton>
|
||||||
on:click={() => {
|
{#if schemaFields.length}
|
||||||
dispatch("change", {
|
<ActionButton
|
||||||
meta: { fields: {} },
|
on:click={() => {
|
||||||
row: {},
|
dispatch("change", {
|
||||||
})
|
meta: { fields: {} },
|
||||||
}}
|
row: {},
|
||||||
>Clear
|
})
|
||||||
</ActionButton>
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</ActionButton>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</PropField>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -388,11 +356,4 @@
|
||||||
.prop-control-wrap :global(.icon.json-slot-icon) {
|
.prop-control-wrap :global(.icon.json-slot-icon) {
|
||||||
right: 1px !important;
|
right: 1px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-fields-btn {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let isTrigger
|
export let isTrigger = false
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
|
||||||
$: filteredTables = $tables.list.filter(table => {
|
$: filteredTables = $tables.list.filter(table => {
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Schema UX Types
|
||||||
|
import FilterSelector from "./FilterSelector.svelte"
|
||||||
|
import ExecuteScript from "./ExecuteScript.svelte"
|
||||||
|
import ExecuteScriptV2 from "./ExecuteScriptV2.svelte"
|
||||||
|
import DateSelector from "./DateSelector.svelte"
|
||||||
|
import SchemaSetup from "./SchemaSetup.svelte"
|
||||||
|
import TableSelector from "./TableSelector.svelte"
|
||||||
|
import CronBuilder from "./CronBuilder.svelte"
|
||||||
|
import FieldSelector from "./FieldSelector.svelte"
|
||||||
|
import FileSelector from "./FileSelector.svelte"
|
||||||
|
import RowSelector from "./RowSelector.svelte"
|
||||||
|
import AutomationSelector from "./AutomationSelector.svelte"
|
||||||
|
import PropField from "./PropField.svelte"
|
||||||
|
import QueryParamSelector from "./QueryParamSelector.svelte"
|
||||||
|
import CodeEditorModal from "./CodeEditorModal.svelte"
|
||||||
|
|
||||||
|
export {
|
||||||
|
FilterSelector,
|
||||||
|
ExecuteScriptV2,
|
||||||
|
DateSelector,
|
||||||
|
SchemaSetup,
|
||||||
|
TableSelector,
|
||||||
|
CronBuilder,
|
||||||
|
FieldSelector,
|
||||||
|
FileSelector,
|
||||||
|
RowSelector,
|
||||||
|
AutomationSelector,
|
||||||
|
PropField,
|
||||||
|
QueryParamSelector,
|
||||||
|
CodeEditorModal,
|
||||||
|
// DEPRECATED
|
||||||
|
ExecuteScript,
|
||||||
|
}
|
|
@ -0,0 +1,330 @@
|
||||||
|
import { DrawerBindableInput } from "@/components/common/bindings"
|
||||||
|
import { automationStore, selectedAutomation } from "@/stores/builder"
|
||||||
|
import { Divider, Helpers, Select } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
AutomationEventType,
|
||||||
|
AutomationStep,
|
||||||
|
AutomationStepType,
|
||||||
|
AutomationTrigger,
|
||||||
|
BaseIOStructure,
|
||||||
|
isTrigger,
|
||||||
|
Row,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import AutomationBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte"
|
||||||
|
import { RowSelector, TableSelector } from "."
|
||||||
|
import { get, Writable } from "svelte/store"
|
||||||
|
import {
|
||||||
|
type StepInputs,
|
||||||
|
type SchemaConfigProps,
|
||||||
|
type FormUpdate,
|
||||||
|
} from "@/types/automations"
|
||||||
|
import RowFetch from "./RowFetch.svelte"
|
||||||
|
|
||||||
|
export const getInputValue = (inputData: StepInputs, key: string) => {
|
||||||
|
const idxInput = inputData as Record<string, unknown>
|
||||||
|
return idxInput?.[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do not enforce required fields.
|
||||||
|
export const getFieldLabel = (
|
||||||
|
key: string,
|
||||||
|
value: BaseIOStructure,
|
||||||
|
isRequired = false
|
||||||
|
) => {
|
||||||
|
const requiredSuffix = isRequired ? "*" : ""
|
||||||
|
const label = `${
|
||||||
|
value.title || (key === "row" ? "Row" : key)
|
||||||
|
} ${requiredSuffix}`
|
||||||
|
return Helpers.capitalise(label)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a custom step layout. This generates an array of ui elements
|
||||||
|
* intended to be rendered as a uniform column
|
||||||
|
* If you intend to have a completely unique UX,
|
||||||
|
*
|
||||||
|
* @param block
|
||||||
|
* @param stepState
|
||||||
|
* @param isTest
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const getCustomStepLayout = (
|
||||||
|
block: AutomationStep | AutomationTrigger | undefined,
|
||||||
|
stepState: Writable<Record<string, any>>,
|
||||||
|
isTest = false
|
||||||
|
): SchemaConfigProps[] | undefined => {
|
||||||
|
if (!block) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the test data for test mode otherwise resolve trigger/step inputs by block
|
||||||
|
const fieldData = isTest
|
||||||
|
? get(selectedAutomation)?.data?.testData
|
||||||
|
: automationStore.actions.getInputData(block)
|
||||||
|
|
||||||
|
const coreDivider = {
|
||||||
|
comp: Divider,
|
||||||
|
props: () => ({
|
||||||
|
noMargin: true,
|
||||||
|
}),
|
||||||
|
wrapped: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIdConfig = (rowIdentifier: string): SchemaConfigProps[] => {
|
||||||
|
const schema =
|
||||||
|
block.type === AutomationStepType.TRIGGER
|
||||||
|
? block.schema.outputs.properties
|
||||||
|
: block.schema.inputs.properties
|
||||||
|
|
||||||
|
const rowIdEntry = schema[rowIdentifier]
|
||||||
|
if (!rowIdEntry) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowIdlabel = getFieldLabel(rowIdentifier, rowIdEntry)
|
||||||
|
|
||||||
|
const layout: SchemaConfigProps[] = [
|
||||||
|
{
|
||||||
|
comp: DrawerBindableInput,
|
||||||
|
title: rowIdlabel,
|
||||||
|
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||||
|
const update = { [rowIdentifier]: e.detail }
|
||||||
|
if (block) {
|
||||||
|
automationStore.actions.requestUpdate(update, block)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: () => {
|
||||||
|
return {
|
||||||
|
panel: AutomationBindingPanel,
|
||||||
|
value: getInputValue(fieldData, rowIdentifier),
|
||||||
|
updateOnChange: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return layout
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRowSelector = (): SchemaConfigProps[] => {
|
||||||
|
const row: Row = getInputValue(fieldData, "row") as Row
|
||||||
|
const meta: Record<string, unknown> = getInputValue(
|
||||||
|
fieldData,
|
||||||
|
"meta"
|
||||||
|
) as Record<string, unknown>
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
comp: RowSelector,
|
||||||
|
wrapped: false,
|
||||||
|
props: () => ({
|
||||||
|
row,
|
||||||
|
meta: meta || {},
|
||||||
|
componentWidth: 280,
|
||||||
|
fullWidth: true,
|
||||||
|
}),
|
||||||
|
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||||
|
if (block) {
|
||||||
|
const metaUpdate = e.detail?.meta as Record<string, unknown>
|
||||||
|
|
||||||
|
automationStore.actions.requestUpdate(
|
||||||
|
{
|
||||||
|
row: e.detail.row,
|
||||||
|
meta: {
|
||||||
|
fields: metaUpdate?.fields || {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
block
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTableSelector = (): SchemaConfigProps[] => {
|
||||||
|
const row: Row = getInputValue(fieldData, "row") as Row
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
comp: TableSelector,
|
||||||
|
title: "Table",
|
||||||
|
onChange: (e: CustomEvent) => {
|
||||||
|
const rowKey = get(stepState)[block.id]?.rowType || "row"
|
||||||
|
automationStore.actions.requestUpdate(
|
||||||
|
{
|
||||||
|
_tableId: e.detail,
|
||||||
|
meta: {},
|
||||||
|
[rowKey]: e.detail
|
||||||
|
? {
|
||||||
|
tableId: e.detail,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
},
|
||||||
|
block
|
||||||
|
)
|
||||||
|
},
|
||||||
|
props: () => ({
|
||||||
|
isTrigger: isTrigger(block),
|
||||||
|
value: row?.tableId ?? "",
|
||||||
|
disabled: isTest,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP
|
||||||
|
if (automationStore.actions.isRowStep(block)) {
|
||||||
|
const layout: SchemaConfigProps[] = [
|
||||||
|
...getTableSelector(),
|
||||||
|
...getIdConfig("rowId"),
|
||||||
|
coreDivider,
|
||||||
|
...getRowSelector(),
|
||||||
|
]
|
||||||
|
return layout
|
||||||
|
} else if (automationStore.actions.isRowTrigger(block) && isTest) {
|
||||||
|
// TRIGGER/TEST - Will be used to replace the test modal
|
||||||
|
const schema = block.schema.outputs.properties
|
||||||
|
|
||||||
|
const getRevConfig = (): SchemaConfigProps[] => {
|
||||||
|
// part of outputs for row type
|
||||||
|
const rowRevEntry = schema["revision"]
|
||||||
|
if (!rowRevEntry) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const rowRevlabel = getFieldLabel("revision", rowRevEntry)
|
||||||
|
const selected = get(selectedAutomation)
|
||||||
|
const triggerOutputs = selected.data?.testData
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
comp: DrawerBindableInput,
|
||||||
|
title: rowRevlabel,
|
||||||
|
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||||
|
const update = { ["revision"]: e.detail }
|
||||||
|
if (block) {
|
||||||
|
automationStore.actions.requestUpdate(update, block)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: () => ({
|
||||||
|
panel: AutomationBindingPanel,
|
||||||
|
value: triggerOutputs ? triggerOutputs["revision"] : undefined,
|
||||||
|
updateOnChange: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// A select to switch from `row` to `oldRow`
|
||||||
|
const getRowTypeConfig = (): SchemaConfigProps[] => {
|
||||||
|
if (block.event !== AutomationEventType.ROW_UPDATE) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!get(stepState)?.[block.id]?.rowType) {
|
||||||
|
stepState.update(state => ({
|
||||||
|
...state,
|
||||||
|
[block.id]: {
|
||||||
|
rowType: "row",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowType = { name: string; id: string }
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
comp: Select,
|
||||||
|
tooltip: `You can configure test data for both the updated row and
|
||||||
|
the old row, if you need it. Just select the one you wish to alter`,
|
||||||
|
title: "Row data",
|
||||||
|
props: () => ({
|
||||||
|
value: get(stepState)?.[block.id].rowType,
|
||||||
|
placeholder: false,
|
||||||
|
getOptionLabel: (type: RowType) => type.name,
|
||||||
|
getOptionValue: (type: RowType) => type.id,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: "row",
|
||||||
|
name: "Updated row",
|
||||||
|
},
|
||||||
|
{ id: "oldRow", name: "Old row" },
|
||||||
|
] as RowType[],
|
||||||
|
}),
|
||||||
|
onChange: (e: CustomEvent) => {
|
||||||
|
if (e.detail === get(stepState)?.[block.id].rowType) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stepState.update(state => ({
|
||||||
|
...state,
|
||||||
|
[block.id]: {
|
||||||
|
rowType: e.detail,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTriggerRowSelector = (): SchemaConfigProps[] => {
|
||||||
|
const rowKey = get(stepState)?.[block.id]?.rowType ?? "row"
|
||||||
|
const rowData: Row = getInputValue(fieldData, rowKey) as Row
|
||||||
|
const row: Row = getInputValue(fieldData, "row") as Row
|
||||||
|
const meta: Record<string, unknown> = getInputValue(
|
||||||
|
fieldData,
|
||||||
|
"meta"
|
||||||
|
) as Record<string, unknown>
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
comp: RowSelector,
|
||||||
|
wrapped: false,
|
||||||
|
props: () => ({
|
||||||
|
componentWidth: 280,
|
||||||
|
row: rowData || {
|
||||||
|
tableId: row?.tableId,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
fields: meta?.oldFields || {},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||||
|
const metaUpdate = e.detail?.meta as Record<string, unknown>
|
||||||
|
|
||||||
|
const update = {
|
||||||
|
[rowKey]: e.detail.row,
|
||||||
|
meta: {
|
||||||
|
fields: meta?.fields || {},
|
||||||
|
oldFields:
|
||||||
|
(metaUpdate?.fields as Record<string, unknown>) || {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (block) {
|
||||||
|
automationStore.actions.requestUpdate(update, block)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const layout: SchemaConfigProps[] = [
|
||||||
|
...getTableSelector(),
|
||||||
|
{
|
||||||
|
comp: RowFetch,
|
||||||
|
title: "Row",
|
||||||
|
props: () => {
|
||||||
|
return {
|
||||||
|
tableId: "ta_bb_employee",
|
||||||
|
meta: getInputValue(fieldData, "meta"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onChange: () => {},
|
||||||
|
},
|
||||||
|
...getIdConfig("id"),
|
||||||
|
...getRevConfig(),
|
||||||
|
...getRowTypeConfig(),
|
||||||
|
coreDivider,
|
||||||
|
...getTriggerRowSelector(),
|
||||||
|
]
|
||||||
|
return layout
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,15 +7,15 @@
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { capitalise } from "@/helpers"
|
import { capitalise } from "@/helpers"
|
||||||
|
|
||||||
export let value
|
export let value = undefined
|
||||||
export let error
|
export let error = undefined
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let allowPublic = true
|
export let allowPublic = true
|
||||||
export let allowRemove = false
|
export let allowRemove = false
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let align
|
export let align = undefined
|
||||||
export let footer = null
|
export let footer = null
|
||||||
export let allowedRoles = null
|
export let allowedRoles = null
|
||||||
export let allowCreator = false
|
export let allowCreator = false
|
||||||
|
@ -135,7 +135,6 @@
|
||||||
{autoWidth}
|
{autoWidth}
|
||||||
{quiet}
|
{quiet}
|
||||||
{disabled}
|
{disabled}
|
||||||
{align}
|
|
||||||
{footer}
|
{footer}
|
||||||
bind:value
|
bind:value
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
|
|
|
@ -49,11 +49,11 @@
|
||||||
|
|
||||||
export let bindings: EnrichedBinding[] = []
|
export let bindings: EnrichedBinding[] = []
|
||||||
export let value: string = ""
|
export let value: string = ""
|
||||||
export let allowHBS = true
|
export let allowHBS: boolean | undefined = true
|
||||||
export let allowJS = false
|
export let allowJS: boolean | undefined = false
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let allowSnippets = true
|
export let allowSnippets = true
|
||||||
export let context = null
|
export let context: Record<any, any> | undefined = undefined
|
||||||
export let snippets: Snippet[] | null = null
|
export let snippets: Snippet[] | null = null
|
||||||
export let autofocusEditor = false
|
export let autofocusEditor = false
|
||||||
export let placeholder: string | null = null
|
export let placeholder: string | null = null
|
||||||
|
@ -128,7 +128,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getModeOptions = (allowHBS: boolean, allowJS: boolean) => {
|
const getModeOptions = (allowHBS = true, allowJS = false) => {
|
||||||
let options = []
|
let options = []
|
||||||
if (allowHBS) {
|
if (allowHBS) {
|
||||||
options.push(BindingMode.Text)
|
options.push(BindingMode.Text)
|
||||||
|
|
|
@ -21,15 +21,15 @@
|
||||||
import SnippetDrawer from "./SnippetDrawer.svelte"
|
import SnippetDrawer from "./SnippetDrawer.svelte"
|
||||||
import UpgradeButton from "@/pages/builder/portal/_components/UpgradeButton.svelte"
|
import UpgradeButton from "@/pages/builder/portal/_components/UpgradeButton.svelte"
|
||||||
|
|
||||||
export let addHelper: (_helper: Helper, _js?: boolean) => void
|
export let addHelper: (_helper: Helper, _js?: boolean) => void = () => {}
|
||||||
export let addBinding: (_binding: EnrichedBinding) => void
|
export let addBinding: (_binding: EnrichedBinding) => void = () => {}
|
||||||
export let addSnippet: (_snippet: Snippet) => void
|
export let addSnippet: (_snippet: Snippet) => void = () => {}
|
||||||
export let bindings: EnrichedBinding[]
|
export let bindings: EnrichedBinding[] | undefined
|
||||||
export let snippets: Snippet[] | null
|
export let snippets: Snippet[] | null = null
|
||||||
export let mode: BindingMode
|
export let mode: BindingMode | undefined = BindingMode.Text
|
||||||
export let allowHelpers: boolean
|
export let allowHelpers: boolean = true
|
||||||
export let allowSnippets: boolean
|
export let allowSnippets: boolean = true
|
||||||
export let context = null
|
export let context: Record<any, any> | undefined = undefined
|
||||||
|
|
||||||
let search = ""
|
let search = ""
|
||||||
let searching = false
|
let searching = false
|
||||||
|
@ -93,7 +93,7 @@
|
||||||
search
|
search
|
||||||
)
|
)
|
||||||
|
|
||||||
function onModeChange(_mode: BindingMode) {
|
function onModeChange(_mode: BindingMode | undefined) {
|
||||||
selectedCategory = null
|
selectedCategory = null
|
||||||
}
|
}
|
||||||
$: onModeChange(mode)
|
$: onModeChange(mode)
|
||||||
|
|
|
@ -36,10 +36,10 @@
|
||||||
export let value: string = ""
|
export let value: string = ""
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let allowSnippets = true
|
export let allowSnippets = true
|
||||||
export let context = null
|
export let context: Record<any, any> | undefined = undefined
|
||||||
export let autofocusEditor = false
|
export let autofocusEditor = false
|
||||||
export let placeholder = null
|
export let placeholder: string | undefined
|
||||||
export let height = 180
|
export let dropdown = DropdownPosition.Absolute
|
||||||
|
|
||||||
let getCaretPosition: CaretPositionFn | undefined
|
let getCaretPosition: CaretPositionFn | undefined
|
||||||
let insertAtPos: InsertAtPositionFn | undefined
|
let insertAtPos: InsertAtPositionFn | undefined
|
||||||
|
@ -126,26 +126,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateValue = (val: any) => {
|
const updateValue = (val: any) => {
|
||||||
dispatch("change", readableToRuntimeBinding(bindings, val))
|
if (!val) {
|
||||||
|
dispatch("change", null)
|
||||||
|
}
|
||||||
|
const update = readableToRuntimeBinding(bindings, encodeJSBinding(val))
|
||||||
|
dispatch("change", update)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChangeJSValue = (e: { detail: string }) => {
|
const onBlurJSValue = (e: { detail: string }) => {
|
||||||
if (!e.detail?.trim()) {
|
// Don't bother saving empty values as JS
|
||||||
// Don't bother saving empty values as JS
|
updateValue(e.detail?.trim())
|
||||||
updateValue(null)
|
|
||||||
} else {
|
|
||||||
updateValue(encodeJSBinding(e.detail))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="code-panel" style="height:{height}px;">
|
<div class="code-panel">
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
{#key jsCompletions}
|
{#key jsCompletions}
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={jsValue || ""}
|
value={jsValue || ""}
|
||||||
on:change={onChangeJSValue}
|
on:blur={onBlurJSValue}
|
||||||
on:blur
|
|
||||||
completions={jsCompletions}
|
completions={jsCompletions}
|
||||||
{bindings}
|
{bindings}
|
||||||
mode={EditorModes.JS}
|
mode={EditorModes.JS}
|
||||||
|
@ -155,7 +154,7 @@
|
||||||
placeholder={placeholder ||
|
placeholder={placeholder ||
|
||||||
"Add bindings by typing $ or use the menu on the right"}
|
"Add bindings by typing $ or use the menu on the right"}
|
||||||
jsBindingWrapping
|
jsBindingWrapping
|
||||||
dropdown={DropdownPosition.Absolute}
|
{dropdown}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
@ -164,6 +163,7 @@
|
||||||
<style>
|
<style>
|
||||||
.code-panel {
|
.code-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Editor */
|
/* Editor */
|
||||||
|
|
|
@ -14,14 +14,15 @@
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let title = "Bindings"
|
export let title = "Bindings"
|
||||||
export let placeholder
|
export let placeholder = undefined
|
||||||
export let label
|
export let label = undefined
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let allowJS = true
|
export let allowJS = true
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let type
|
export let type = undefined
|
||||||
export let schema
|
export let schema = undefined
|
||||||
|
|
||||||
export let allowHBS = true
|
export let allowHBS = true
|
||||||
export let context = {}
|
export let context = {}
|
||||||
|
|
||||||
|
@ -230,7 +231,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
right: 1px;
|
|
||||||
bottom: 1px;
|
bottom: 1px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -239,8 +239,6 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-left: 1px solid var(--spectrum-alias-border-color);
|
border-left: 1px solid var(--spectrum-alias-border-color);
|
||||||
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
|
|
||||||
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
|
|
||||||
width: 31px;
|
width: 31px;
|
||||||
color: var(--spectrum-alias-text-color);
|
color: var(--spectrum-alias-text-color);
|
||||||
background-color: var(--spectrum-global-color-gray-75);
|
background-color: var(--spectrum-global-color-gray-75);
|
||||||
|
|
|
@ -7,15 +7,15 @@
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
} from "@/dataBinding"
|
} from "@/dataBinding"
|
||||||
|
|
||||||
export let schemaFields
|
export let schemaFields = undefined
|
||||||
export let filters
|
export let filters = undefined
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let panel = ClientBindingPanel
|
export let panel = ClientBindingPanel
|
||||||
export let allowBindings = true
|
export let allowBindings = true
|
||||||
export let allowOnEmpty
|
export let allowOnEmpty = undefined
|
||||||
export let datasource
|
export let datasource = undefined
|
||||||
export let builderType
|
export let builderType = undefined
|
||||||
export let docsURL
|
export let docsURL = undefined
|
||||||
export let evaluationContext = {}
|
export let evaluationContext = {}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -15,24 +15,23 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let defaults
|
export let defaults = undefined
|
||||||
export let object = defaults || {}
|
export let object = defaults || {}
|
||||||
export let activity = {}
|
export let activity = {}
|
||||||
export let readOnly
|
export let readOnly = false
|
||||||
export let noAddButton
|
export let noAddButton = false
|
||||||
export let name
|
export let name = ""
|
||||||
export let headings = false
|
export let headings = false
|
||||||
export let options
|
export let options = undefined
|
||||||
export let toggle
|
export let toggle = false
|
||||||
export let keyPlaceholder = "Key"
|
export let keyPlaceholder = "Key"
|
||||||
export let valuePlaceholder = "Value"
|
export let valuePlaceholder = "Value"
|
||||||
export let valueHeading
|
export let valueHeading = ""
|
||||||
export let keyHeading
|
export let keyHeading = ""
|
||||||
export let tooltip
|
export let tooltip = ""
|
||||||
export let menuItems
|
export let menuItems = []
|
||||||
export let showMenu = false
|
export let showMenu = false
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let bindingDrawerLeft
|
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let customButtonText = null
|
export let customButtonText = null
|
||||||
export let keyBindings = false
|
export let keyBindings = false
|
||||||
|
@ -140,7 +139,6 @@
|
||||||
value={field.name}
|
value={field.name}
|
||||||
{allowJS}
|
{allowJS}
|
||||||
{allowHelpers}
|
{allowHelpers}
|
||||||
drawerLeft={bindingDrawerLeft}
|
|
||||||
{context}
|
{context}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -167,7 +165,6 @@
|
||||||
value={field.value}
|
value={field.value}
|
||||||
{allowJS}
|
{allowJS}
|
||||||
{allowHelpers}
|
{allowHelpers}
|
||||||
drawerLeft={bindingDrawerLeft}
|
|
||||||
{context}
|
{context}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
keyPlaceholder="Binding name"
|
keyPlaceholder="Binding name"
|
||||||
valuePlaceholder="Default"
|
valuePlaceholder="Default"
|
||||||
bindings={[...userBindings]}
|
bindings={[...userBindings]}
|
||||||
bindingDrawerLeft="260px"
|
|
||||||
allowHelpers={false}
|
allowHelpers={false}
|
||||||
on:change
|
on:change
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -582,7 +582,6 @@
|
||||||
...globalDynamicRequestBindings,
|
...globalDynamicRequestBindings,
|
||||||
...dataSourceStaticBindings,
|
...dataSourceStaticBindings,
|
||||||
]}
|
]}
|
||||||
bindingDrawerLeft="260px"
|
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Params">
|
<Tab title="Params">
|
||||||
|
@ -591,7 +590,6 @@
|
||||||
name="param"
|
name="param"
|
||||||
headings
|
headings
|
||||||
bindings={mergedBindings}
|
bindings={mergedBindings}
|
||||||
bindingDrawerLeft="260px"
|
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Headers">
|
<Tab title="Headers">
|
||||||
|
@ -602,7 +600,6 @@
|
||||||
name="header"
|
name="header"
|
||||||
headings
|
headings
|
||||||
bindings={mergedBindings}
|
bindings={mergedBindings}
|
||||||
bindingDrawerLeft="260px"
|
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Body">
|
<Tab title="Body">
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
import AutomationPanel from "@/components/automation/AutomationPanel/AutomationPanel.svelte"
|
import AutomationPanel from "@/components/automation/AutomationPanel/AutomationPanel.svelte"
|
||||||
import CreateAutomationModal from "@/components/automation/AutomationPanel/CreateAutomationModal.svelte"
|
import CreateAutomationModal from "@/components/automation/AutomationPanel/CreateAutomationModal.svelte"
|
||||||
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import TestPanel from "@/components/automation/AutomationBuilder/TestPanel.svelte"
|
import { onDestroy } from "svelte"
|
||||||
import { onDestroy, onMount } from "svelte"
|
|
||||||
import { syncURLToState } from "@/helpers/urlStateSync"
|
import { syncURLToState } from "@/helpers/urlStateSync"
|
||||||
import * as routify from "@roxi/routify"
|
import * as routify from "@roxi/routify"
|
||||||
import {
|
import {
|
||||||
|
@ -12,8 +11,10 @@
|
||||||
automationStore,
|
automationStore,
|
||||||
selectedAutomation,
|
selectedAutomation,
|
||||||
} from "@/stores/builder"
|
} from "@/stores/builder"
|
||||||
|
import StepPanel from "@/components/automation/AutomationBuilder/StepPanel.svelte"
|
||||||
|
|
||||||
$: automationId = $selectedAutomation?.data?._id
|
$: automationId = $selectedAutomation?.data?._id
|
||||||
|
$: blockRefs = $selectedAutomation.blockRefs
|
||||||
$: builderStore.selectResource(automationId)
|
$: builderStore.selectResource(automationId)
|
||||||
|
|
||||||
const stopSyncing = syncURLToState({
|
const stopSyncing = syncURLToState({
|
||||||
|
@ -29,15 +30,6 @@
|
||||||
let modal
|
let modal
|
||||||
let webhookModal
|
let webhookModal
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await automationStore.actions.initAppSelf()
|
|
||||||
|
|
||||||
// Init the binding evaluation context
|
|
||||||
automationStore.actions.initContext()
|
|
||||||
|
|
||||||
$automationStore.showTestPanel = false
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(stopSyncing)
|
onDestroy(stopSyncing)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -70,15 +62,16 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $automationStore.showTestPanel}
|
{#if blockRefs[$automationStore.selectedNodeId] && $automationStore.selectedNodeId}
|
||||||
<div class="setup">
|
<div class="step-panel">
|
||||||
<TestPanel automation={$selectedAutomation.data} />
|
<StepPanel />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<CreateAutomationModal {webhookModal} />
|
<CreateAutomationModal {webhookModal} />
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal bind:this={webhookModal} width="30%">
|
<Modal bind:this={webhookModal}>
|
||||||
<CreateWebhookModal />
|
<CreateWebhookModal />
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
@ -115,16 +108,17 @@
|
||||||
.main {
|
.main {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
.setup {
|
|
||||||
padding-top: 9px;
|
.step-panel {
|
||||||
border-left: var(--border-light);
|
border-left: var(--border-light);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--spacing-l);
|
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
grid-column: 3;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
grid-column: 3;
|
||||||
|
width: 360px;
|
||||||
|
max-width: 360px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -147,6 +147,13 @@
|
||||||
await automationStore.actions.fetch()
|
await automationStore.actions.fetch()
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
const shouldOpen = params.get("open") === ERROR
|
const shouldOpen = params.get("open") === ERROR
|
||||||
|
const defaultAutoId = params.get("automationId")
|
||||||
|
const defaultAuto = $automationStore.automations.find(
|
||||||
|
auto => auto._id === defaultAutoId || undefined
|
||||||
|
)
|
||||||
|
|
||||||
|
automationId = defaultAuto?._id || undefined
|
||||||
|
|
||||||
if (shouldOpen) {
|
if (shouldOpen) {
|
||||||
status = ERROR
|
status = ERROR
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,213 @@
|
||||||
|
import {
|
||||||
|
type AutomationStep,
|
||||||
|
type AutomationStepInputs,
|
||||||
|
type AutomationTrigger,
|
||||||
|
type AutomationTriggerInputs,
|
||||||
|
type BaseIOStructure,
|
||||||
|
type UISearchFilter,
|
||||||
|
type TestAutomationResponse,
|
||||||
|
type AppSelfResponse,
|
||||||
|
type Automation,
|
||||||
|
type BlockDefinitions,
|
||||||
|
type BlockRef,
|
||||||
|
AutomationActionStepId,
|
||||||
|
AutomationTriggerStepId,
|
||||||
|
AutomationCustomIOType,
|
||||||
|
AutomationIOType,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { SvelteComponent } from "svelte"
|
||||||
|
|
||||||
|
export enum DataMode {
|
||||||
|
INPUT = "data_in",
|
||||||
|
OUTPUT = "data_out",
|
||||||
|
ERRORS = "errors",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SchemaFieldTypes {
|
||||||
|
JSON = "json",
|
||||||
|
ENUM = "enum",
|
||||||
|
BOOL = "boolean",
|
||||||
|
DATE = "date",
|
||||||
|
FILE = "file",
|
||||||
|
FILTER = "filter",
|
||||||
|
CRON = "cron",
|
||||||
|
FIELDS = "fields",
|
||||||
|
TABLE = "table",
|
||||||
|
COLUMN = "column",
|
||||||
|
AUTOMATION_FIELDS = "automation_fields",
|
||||||
|
WEBHOOK_URL = "webhook_url",
|
||||||
|
TRIGGER_SCHEMA = "trigger_schema",
|
||||||
|
LOOP_OPTION = "loop_option",
|
||||||
|
CODE = "code",
|
||||||
|
CODE_V2 = "code_v2",
|
||||||
|
STRING = "string",
|
||||||
|
QUERY_PARAMS = "query_params",
|
||||||
|
QUERY_LIMIT = "query_limit",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KeyValuePair = {
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required for filter fields. The have a definition entry
|
||||||
|
// as well as a configuration
|
||||||
|
export type DynamicProperties = {
|
||||||
|
[property: `${string}-def`]: UISearchFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form field update by property key
|
||||||
|
export type FormUpdate = Record<string, unknown>
|
||||||
|
|
||||||
|
export type AutomationSchemaConfig = Record<SchemaFieldTypes, SchemaConfigProps>
|
||||||
|
|
||||||
|
export type FieldProps = {
|
||||||
|
key: string
|
||||||
|
field: BaseIOStructure
|
||||||
|
block: AutomationStep | AutomationTrigger
|
||||||
|
value: any
|
||||||
|
meta: any
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unused for the moment
|
||||||
|
export type InputMeta = {
|
||||||
|
meta?: AutomationStepInputMeta<AutomationActionStepId>
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is still Attachment Specific and technically reusable.
|
||||||
|
export type FileSelectorMeta = { useAttachmentBinding: boolean }
|
||||||
|
|
||||||
|
export type AutomationStepInputMeta<T extends AutomationActionStepId> =
|
||||||
|
T extends AutomationActionStepId.SEND_EMAIL_SMTP
|
||||||
|
? { meta: FileSelectorMeta }
|
||||||
|
: { meta?: Record<string, unknown> }
|
||||||
|
|
||||||
|
export type StepInputs =
|
||||||
|
| AutomationStepInputs<AutomationActionStepId> //& InputMeta
|
||||||
|
| AutomationTriggerInputs<AutomationTriggerStepId> //& InputMeta
|
||||||
|
| undefined
|
||||||
|
|
||||||
|
export const RowTriggers = [
|
||||||
|
AutomationTriggerStepId.ROW_UPDATED,
|
||||||
|
AutomationTriggerStepId.ROW_SAVED,
|
||||||
|
AutomationTriggerStepId.ROW_DELETED,
|
||||||
|
AutomationTriggerStepId.ROW_ACTION,
|
||||||
|
]
|
||||||
|
|
||||||
|
export const RowSteps = [
|
||||||
|
AutomationActionStepId.CREATE_ROW,
|
||||||
|
AutomationActionStepId.UPDATE_ROW,
|
||||||
|
AutomationActionStepId.DELETE_ROW,
|
||||||
|
]
|
||||||
|
|
||||||
|
export const FilterableRowTriggers = [
|
||||||
|
AutomationTriggerStepId.ROW_UPDATED,
|
||||||
|
AutomationTriggerStepId.ROW_SAVED,
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to define how to represent automation setting
|
||||||
|
* forms
|
||||||
|
*/
|
||||||
|
export interface SchemaConfigProps {
|
||||||
|
comp: typeof SvelteComponent<any>
|
||||||
|
onChange?: (e: CustomEvent) => void
|
||||||
|
props?: (opts?: FieldProps) => Record<string, unknown>
|
||||||
|
fullWidth?: boolean
|
||||||
|
title?: string
|
||||||
|
tooltip?: string
|
||||||
|
wrapped?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationState {
|
||||||
|
automations: Automation[]
|
||||||
|
testResults?: TestAutomationResponse
|
||||||
|
showTestModal: boolean
|
||||||
|
blockDefinitions: BlockDefinitions
|
||||||
|
selectedAutomationId: string | null
|
||||||
|
appSelf?: AppSelfResponse
|
||||||
|
selectedNodeId?: string
|
||||||
|
selectedNodeMode?: DataMode
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DerivedAutomationState extends AutomationState {
|
||||||
|
data?: Automation
|
||||||
|
blockRefs: Record<string, BlockRef>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BlockProperties - Direct mapping of customType to SchemaFieldTypes
|
||||||
|
*/
|
||||||
|
export const customTypeToSchema: Record<string, SchemaFieldTypes> = {
|
||||||
|
[AutomationCustomIOType.TRIGGER_SCHEMA]: SchemaFieldTypes.TRIGGER_SCHEMA,
|
||||||
|
[AutomationCustomIOType.TABLE]: SchemaFieldTypes.TABLE,
|
||||||
|
[AutomationCustomIOType.COLUMN]: SchemaFieldTypes.COLUMN,
|
||||||
|
[AutomationCustomIOType.CRON]: SchemaFieldTypes.CRON,
|
||||||
|
[AutomationCustomIOType.LOOP_OPTION]: SchemaFieldTypes.LOOP_OPTION,
|
||||||
|
[AutomationCustomIOType.AUTOMATION_FIELDS]:
|
||||||
|
SchemaFieldTypes.AUTOMATION_FIELDS,
|
||||||
|
[AutomationCustomIOType.WEBHOOK_URL]: SchemaFieldTypes.WEBHOOK_URL,
|
||||||
|
[AutomationCustomIOType.QUERY_LIMIT]: SchemaFieldTypes.QUERY_LIMIT,
|
||||||
|
["fields"]: SchemaFieldTypes.FIELDS,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BlockProperties - Direct mapping of type to SchemaFieldTypes
|
||||||
|
*/
|
||||||
|
export const typeToSchema: Partial<Record<AutomationIOType, SchemaFieldTypes>> =
|
||||||
|
{
|
||||||
|
[AutomationIOType.BOOLEAN]: SchemaFieldTypes.BOOL,
|
||||||
|
[AutomationIOType.DATE]: SchemaFieldTypes.DATE,
|
||||||
|
[AutomationIOType.JSON]: SchemaFieldTypes.JSON,
|
||||||
|
[AutomationIOType.ATTACHMENT]: SchemaFieldTypes.FILE,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FlowItem - Status type enums for grading issues
|
||||||
|
*/
|
||||||
|
export enum FlowStatusType {
|
||||||
|
INFO = "info",
|
||||||
|
WARN = "warn",
|
||||||
|
ERROR = "error",
|
||||||
|
SUCCESS = "success",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FlowItemStatus - Message structure for building information
|
||||||
|
* pills that hover above the steps in the automation tree
|
||||||
|
*/
|
||||||
|
export type FlowItemStatus = {
|
||||||
|
message: string
|
||||||
|
icon: string
|
||||||
|
type: FlowStatusType
|
||||||
|
tooltip?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BlockData - Status type designation for errors or issues arising from
|
||||||
|
* test runs or other automation concerns
|
||||||
|
*/
|
||||||
|
export enum BlockStatusType {
|
||||||
|
INFO = "info",
|
||||||
|
WARN = "warn",
|
||||||
|
ERROR = "error",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BlockData - Source type used to discern the source of the issues
|
||||||
|
*/
|
||||||
|
export enum BlockStatusSource {
|
||||||
|
AUTOMATION_RESULTS = "results",
|
||||||
|
VALIDATION = "validation",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BlockData - Message structure for building out issues to be displayed
|
||||||
|
* in the Step Panel in automations
|
||||||
|
*/
|
||||||
|
export type BlockStatus = {
|
||||||
|
message: string
|
||||||
|
type: BlockStatusType
|
||||||
|
source?: BlockStatusSource
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"extends": "./tsconfig.build.json",
|
"extends": "./tsconfig.build.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
|
|
@ -16,6 +16,9 @@ import {
|
||||||
FieldType,
|
FieldType,
|
||||||
FilterCondition,
|
FilterCondition,
|
||||||
isDidNotTriggerResponse,
|
isDidNotTriggerResponse,
|
||||||
|
RowActionTriggerInputs,
|
||||||
|
RowCreatedTriggerInputs,
|
||||||
|
RowDeletedTriggerInputs,
|
||||||
SettingsConfig,
|
SettingsConfig,
|
||||||
Table,
|
Table,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
@ -259,7 +262,10 @@ describe("/automations", () => {
|
||||||
it("tests the automation successfully", async () => {
|
it("tests the automation successfully", async () => {
|
||||||
let table = await config.createTable()
|
let table = await config.createTable()
|
||||||
let automation = newAutomation()
|
let automation = newAutomation()
|
||||||
automation.definition.trigger.inputs.tableId = table._id
|
const triggerInputs = automation.definition.trigger
|
||||||
|
.inputs as RowCreatedTriggerInputs
|
||||||
|
triggerInputs.tableId = table._id!
|
||||||
|
|
||||||
automation.definition.steps[0].inputs = {
|
automation.definition.steps[0].inputs = {
|
||||||
row: {
|
row: {
|
||||||
name: "{{trigger.row.name}}",
|
name: "{{trigger.row.name}}",
|
||||||
|
@ -487,22 +493,28 @@ describe("/automations", () => {
|
||||||
.serverLog({ text: "test" })
|
.serverLog({ text: "test" })
|
||||||
.save()
|
.save()
|
||||||
|
|
||||||
automation.definition.trigger.inputs.tableId = "newTableId"
|
const triggerInputs = automation.definition.trigger
|
||||||
|
.inputs as RowDeletedTriggerInputs
|
||||||
|
|
||||||
|
triggerInputs.tableId = "newTableId"
|
||||||
const { automation: updatedAutomation } =
|
const { automation: updatedAutomation } =
|
||||||
await config.api.automation.update(automation)
|
await config.api.automation.update(automation)
|
||||||
|
|
||||||
expect(updatedAutomation.definition.trigger.inputs.tableId).toEqual(
|
const updatedTriggerInputs = updatedAutomation.definition.trigger
|
||||||
"newTableId"
|
.inputs as RowDeletedTriggerInputs
|
||||||
)
|
|
||||||
|
expect(updatedTriggerInputs.tableId).toEqual("newTableId")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("cannot update a readonly field", async () => {
|
it("cannot update a readonly field", async () => {
|
||||||
const { automation } = await createAutomationBuilder(config)
|
const { automation } = await createAutomationBuilder(config)
|
||||||
.onRowAction({ tableId: "tableId" })
|
.onRowAction({ tableId: "tableId", rowActionId: "someId" })
|
||||||
.serverLog({ text: "test" })
|
.serverLog({ text: "test" })
|
||||||
.save()
|
.save()
|
||||||
|
|
||||||
automation.definition.trigger.inputs.tableId = "newTableId"
|
const triggerInputs = automation.definition.trigger
|
||||||
|
.inputs as RowActionTriggerInputs
|
||||||
|
triggerInputs.tableId = "newTableId"
|
||||||
await config.api.automation.update(automation, {
|
await config.api.automation.update(automation, {
|
||||||
status: 400,
|
status: 400,
|
||||||
body: {
|
body: {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||||
import { captureAutomationResults } from "../utilities"
|
import { captureAutomationResults } from "../utilities"
|
||||||
import { Automation } from "@budibase/types"
|
import { Automation, AutomationIOType } from "@budibase/types"
|
||||||
|
|
||||||
describe("app action trigger", () => {
|
describe("app action trigger", () => {
|
||||||
const config = new TestConfiguration()
|
const config = new TestConfiguration()
|
||||||
|
@ -48,7 +48,11 @@ describe("app action trigger", () => {
|
||||||
it("should correct coerce values based on the schema", async () => {
|
it("should correct coerce values based on the schema", async () => {
|
||||||
const { automation } = await createAutomationBuilder(config)
|
const { automation } = await createAutomationBuilder(config)
|
||||||
.onAppAction({
|
.onAppAction({
|
||||||
fields: { text: "string", number: "number", boolean: "boolean" },
|
fields: {
|
||||||
|
text: AutomationIOType.STRING,
|
||||||
|
number: AutomationIOType.NUMBER,
|
||||||
|
boolean: AutomationIOType.BOOLEAN,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.serverLog({
|
.serverLog({
|
||||||
text: "{{ fields.text }} {{ fields.number }} {{ fields.boolean }}",
|
text: "{{ fields.text }} {{ fields.number }} {{ fields.boolean }}",
|
||||||
|
|
|
@ -23,6 +23,8 @@ import {
|
||||||
AutomationResults,
|
AutomationResults,
|
||||||
DidNotTriggerResponse,
|
DidNotTriggerResponse,
|
||||||
Table,
|
Table,
|
||||||
|
AutomationTriggerStepId,
|
||||||
|
AutomationTriggerInputs,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { executeInThread } from "../threads/automation"
|
import { executeInThread } from "../threads/automation"
|
||||||
import { dataFilters, sdk } from "@budibase/shared-core"
|
import { dataFilters, sdk } from "@budibase/shared-core"
|
||||||
|
@ -35,6 +37,26 @@ const JOB_OPTS = {
|
||||||
import * as automationUtils from "../automations/automationUtils"
|
import * as automationUtils from "../automations/automationUtils"
|
||||||
import { doesTableExist } from "../sdk/app/tables/getters"
|
import { doesTableExist } from "../sdk/app/tables/getters"
|
||||||
|
|
||||||
|
type RowTriggerStepId =
|
||||||
|
| AutomationTriggerStepId.ROW_ACTION
|
||||||
|
| AutomationTriggerStepId.ROW_DELETED
|
||||||
|
| AutomationTriggerStepId.ROW_SAVED
|
||||||
|
| AutomationTriggerStepId.ROW_UPDATED
|
||||||
|
|
||||||
|
type RowTriggerInputs = Extract<
|
||||||
|
AutomationTriggerInputs<RowTriggerStepId>,
|
||||||
|
{ tableId: string }
|
||||||
|
>
|
||||||
|
|
||||||
|
type RowFilterStepId =
|
||||||
|
| AutomationTriggerStepId.ROW_SAVED
|
||||||
|
| AutomationTriggerStepId.ROW_UPDATED
|
||||||
|
|
||||||
|
type RowFilterInputs = Extract<
|
||||||
|
AutomationTriggerInputs<RowFilterStepId>,
|
||||||
|
{ filters?: SearchFilters }
|
||||||
|
>
|
||||||
|
|
||||||
async function getAllAutomations() {
|
async function getAllAutomations() {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
let automations = await db.allDocs<Automation>(
|
let automations = await db.allDocs<Automation>(
|
||||||
|
@ -64,11 +86,14 @@ async function queueRelevantRowAutomations(
|
||||||
// make sure it is the correct table ID as well
|
// make sure it is the correct table ID as well
|
||||||
automations = automations.filter(automation => {
|
automations = automations.filter(automation => {
|
||||||
const trigger = automation.definition.trigger
|
const trigger = automation.definition.trigger
|
||||||
|
|
||||||
|
const triggerInputs = trigger?.inputs as RowTriggerInputs
|
||||||
|
|
||||||
return (
|
return (
|
||||||
trigger &&
|
trigger &&
|
||||||
trigger.event === eventType &&
|
trigger.event === eventType &&
|
||||||
!automation.disabled &&
|
!automation.disabled &&
|
||||||
trigger?.inputs?.tableId === event.row.tableId
|
triggerInputs?.tableId === event.row.tableId
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -183,7 +208,9 @@ export async function externalTrigger(
|
||||||
) {
|
) {
|
||||||
// values are likely to be submitted as strings, so we shall convert to correct type
|
// values are likely to be submitted as strings, so we shall convert to correct type
|
||||||
const coercedFields: any = {}
|
const coercedFields: any = {}
|
||||||
const fields = automation.definition.trigger.inputs.fields
|
const triggerInputs = automation.definition.trigger.inputs
|
||||||
|
const fields =
|
||||||
|
triggerInputs && "fields" in triggerInputs ? triggerInputs.fields : {}
|
||||||
for (const key of Object.keys(fields || {})) {
|
for (const key of Object.keys(fields || {})) {
|
||||||
coercedFields[key] = coerce(params.fields[key], fields[key])
|
coercedFields[key] = coerce(params.fields[key], fields[key])
|
||||||
}
|
}
|
||||||
|
@ -269,8 +296,9 @@ async function checkTriggerFilters(
|
||||||
event: { row: Row; oldRow: Row }
|
event: { row: Row; oldRow: Row }
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const trigger = automation.definition.trigger
|
const trigger = automation.definition.trigger
|
||||||
const filters = trigger?.inputs?.filters
|
const triggerInputs = trigger.inputs as RowFilterInputs
|
||||||
const tableId = trigger?.inputs?.tableId
|
const filters = triggerInputs?.filters
|
||||||
|
const tableId = triggerInputs?.tableId
|
||||||
|
|
||||||
if (!filters) {
|
if (!filters) {
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
import { Thread, ThreadType } from "../threads"
|
import { Thread, ThreadType } from "../threads"
|
||||||
import { automations } from "@budibase/shared-core"
|
|
||||||
import { automationQueue } from "./bullboard"
|
import { automationQueue } from "./bullboard"
|
||||||
import { updateEntityMetadata } from "../utilities"
|
import { updateEntityMetadata } from "../utilities"
|
||||||
import { context, db as dbCore, utils } from "@budibase/backend-core"
|
import { context, db as dbCore, utils } from "@budibase/backend-core"
|
||||||
import { getAutomationMetadataParams } from "../db/utils"
|
import { getAutomationMetadataParams } from "../db/utils"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import { Automation, AutomationJob, MetadataType } from "@budibase/types"
|
import {
|
||||||
|
Automation,
|
||||||
|
AutomationJob,
|
||||||
|
CronTriggerInputs,
|
||||||
|
isCronTrigger,
|
||||||
|
MetadataType,
|
||||||
|
} from "@budibase/types"
|
||||||
import { automationsEnabled } from "../features"
|
import { automationsEnabled } from "../features"
|
||||||
import { helpers, REBOOT_CRON } from "@budibase/shared-core"
|
import { helpers, REBOOT_CRON } from "@budibase/shared-core"
|
||||||
import tracer from "dd-trace"
|
import tracer from "dd-trace"
|
||||||
import { JobId } from "bull"
|
import { JobId } from "bull"
|
||||||
|
|
||||||
const CRON_STEP_ID = automations.triggers.definitions.CRON.stepId
|
|
||||||
let Runner: Thread
|
let Runner: Thread
|
||||||
if (automationsEnabled()) {
|
if (automationsEnabled()) {
|
||||||
Runner = new Thread(ThreadType.AUTOMATION)
|
Runner = new Thread(ThreadType.AUTOMATION)
|
||||||
|
@ -60,7 +64,11 @@ export async function processEvent(job: AutomationJob) {
|
||||||
const task = async () => {
|
const task = async () => {
|
||||||
try {
|
try {
|
||||||
return await tracer.trace("task", async () => {
|
return await tracer.trace("task", async () => {
|
||||||
if (isCronTrigger(job.data.automation) && !job.data.event.timestamp) {
|
const trigger = job.data.automation
|
||||||
|
? job.data.automation.definition.trigger
|
||||||
|
: null
|
||||||
|
const isCron = trigger && isCronTrigger(trigger)
|
||||||
|
if (isCron && !job.data.event.timestamp) {
|
||||||
// Requires the timestamp at run time
|
// Requires the timestamp at run time
|
||||||
job.data.event.timestamp = Date.now()
|
job.data.event.timestamp = Date.now()
|
||||||
}
|
}
|
||||||
|
@ -147,17 +155,14 @@ export async function clearMetadata() {
|
||||||
await db.bulkDocs(automationMetadata)
|
await db.bulkDocs(automationMetadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCronTrigger(auto: Automation) {
|
|
||||||
return (
|
|
||||||
auto &&
|
|
||||||
auto.definition.trigger &&
|
|
||||||
auto.definition.trigger.stepId === CRON_STEP_ID
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isRebootTrigger(auto: Automation) {
|
export function isRebootTrigger(auto: Automation) {
|
||||||
const trigger = auto ? auto.definition.trigger : null
|
const trigger = auto ? auto.definition.trigger : null
|
||||||
return isCronTrigger(auto) && trigger?.inputs.cron === REBOOT_CRON
|
const isCron = trigger && isCronTrigger(trigger)
|
||||||
|
if (!isCron) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const inputs = trigger?.inputs as CronTriggerInputs
|
||||||
|
return inputs.cron === REBOOT_CRON
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -171,12 +176,13 @@ export async function enableCronTrigger(appId: any, automation: Automation) {
|
||||||
|
|
||||||
// need to create cron job
|
// need to create cron job
|
||||||
if (
|
if (
|
||||||
isCronTrigger(automation) &&
|
trigger &&
|
||||||
|
isCronTrigger(trigger) &&
|
||||||
!isRebootTrigger(automation) &&
|
!isRebootTrigger(automation) &&
|
||||||
!automation.disabled &&
|
!automation.disabled
|
||||||
trigger?.inputs.cron
|
|
||||||
) {
|
) {
|
||||||
const cronExp = trigger.inputs.cron
|
const inputs = trigger.inputs as CronTriggerInputs
|
||||||
|
const cronExp = inputs.cron
|
||||||
const validation = helpers.cron.validate(cronExp)
|
const validation = helpers.cron.validate(cronExp)
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
|
@ -40,7 +40,7 @@ function cleanAutomationInputs(automation: Automation) {
|
||||||
if (step == null) {
|
if (step == null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for (const key of Object.keys(step.inputs)) {
|
for (const key of Object.keys(step.inputs || {})) {
|
||||||
const inputName = key as keyof typeof step.inputs
|
const inputName = key as keyof typeof step.inputs
|
||||||
if (!step.inputs[inputName] || step.inputs[inputName] === "") {
|
if (!step.inputs[inputName] || step.inputs[inputName] === "") {
|
||||||
delete step.inputs[inputName]
|
delete step.inputs[inputName]
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
AutomationTriggerStepId,
|
AutomationTriggerStepId,
|
||||||
RowAttachment,
|
RowAttachment,
|
||||||
FieldType,
|
FieldType,
|
||||||
|
WebhookTriggerInputs,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { getAutomationParams } from "../../../db/utils"
|
import { getAutomationParams } from "../../../db/utils"
|
||||||
import { budibaseTempDir } from "../../../utilities/budibaseDir"
|
import { budibaseTempDir } from "../../../utilities/budibaseDir"
|
||||||
|
@ -99,7 +100,7 @@ async function updateAutomations(prodAppId: string, db: Database) {
|
||||||
if (
|
if (
|
||||||
automation.definition.trigger?.stepId === AutomationTriggerStepId.WEBHOOK
|
automation.definition.trigger?.stepId === AutomationTriggerStepId.WEBHOOK
|
||||||
) {
|
) {
|
||||||
const old = automation.definition.trigger.inputs
|
const old = automation.definition.trigger.inputs as WebhookTriggerInputs
|
||||||
automation.definition.trigger.inputs = {
|
automation.definition.trigger.inputs = {
|
||||||
schemaUrl: old.schemaUrl.replace(oldDevAppId, devAppId),
|
schemaUrl: old.schemaUrl.replace(oldDevAppId, devAppId),
|
||||||
triggerUrl: old.triggerUrl.replace(oldProdAppId, prodAppId),
|
triggerUrl: old.triggerUrl.replace(oldProdAppId, prodAppId),
|
||||||
|
|
|
@ -28,7 +28,7 @@ export const definition: AutomationStepDefinition = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
customType: AutomationCustomIOType.AUTOMATION_FIELDS,
|
customType: AutomationCustomIOType.AUTOMATION_FIELDS,
|
||||||
title: "automatioFields",
|
title: "Automation Fields",
|
||||||
required: ["automationId"],
|
required: ["automationId"],
|
||||||
},
|
},
|
||||||
timeout: {
|
timeout: {
|
||||||
|
|
|
@ -320,6 +320,7 @@ export type WebhookTriggerOutputs = Record<string, any> & {
|
||||||
|
|
||||||
export type RowActionTriggerInputs = {
|
export type RowActionTriggerInputs = {
|
||||||
tableId: string
|
tableId: string
|
||||||
|
rowActionId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RowActionTriggerOutputs = {
|
export type RowActionTriggerOutputs = {
|
||||||
|
|
|
@ -148,20 +148,20 @@ export interface BaseIOStructure {
|
||||||
dependsOn?: string
|
dependsOn?: string
|
||||||
enum?: string[]
|
enum?: string[]
|
||||||
pretty?: string[]
|
pretty?: string[]
|
||||||
properties?: {
|
properties?: AutomationIOProps
|
||||||
[key: string]: BaseIOStructure
|
|
||||||
}
|
|
||||||
required?: string[]
|
required?: string[]
|
||||||
readonly?: true
|
readonly?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InputOutputBlock {
|
export interface InputOutputBlock {
|
||||||
properties: {
|
properties: AutomationIOProps
|
||||||
[key: string]: BaseIOStructure
|
|
||||||
}
|
|
||||||
required?: string[]
|
required?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AutomationIOProps {
|
||||||
|
[key: string]: BaseIOStructure
|
||||||
|
}
|
||||||
|
|
||||||
export enum AutomationFeature {
|
export enum AutomationFeature {
|
||||||
LOOPING = "LOOPING",
|
LOOPING = "LOOPING",
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
InputOutputBlock,
|
InputOutputBlock,
|
||||||
AutomationTriggerStepId,
|
AutomationTriggerStepId,
|
||||||
AutomationEventType,
|
AutomationEventType,
|
||||||
|
AutomationIOType,
|
||||||
} from "./automation"
|
} from "./automation"
|
||||||
import {
|
import {
|
||||||
CollectStepInputs,
|
CollectStepInputs,
|
||||||
|
@ -57,6 +58,7 @@ import {
|
||||||
RowCreatedTriggerOutputs,
|
RowCreatedTriggerOutputs,
|
||||||
RowUpdatedTriggerOutputs,
|
RowUpdatedTriggerOutputs,
|
||||||
WebhookTriggerOutputs,
|
WebhookTriggerOutputs,
|
||||||
|
RowActionTriggerInputs,
|
||||||
} from "./StepInputsOutputs"
|
} from "./StepInputsOutputs"
|
||||||
|
|
||||||
export type ActionImplementations<T extends Hosting> = {
|
export type ActionImplementations<T extends Hosting> = {
|
||||||
|
@ -383,6 +385,30 @@ export function isAppTrigger(
|
||||||
return step.stepId === AutomationTriggerStepId.APP
|
return step.stepId === AutomationTriggerStepId.APP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isFilterStep(
|
||||||
|
step: AutomationStep | AutomationTrigger
|
||||||
|
): step is FilterStep {
|
||||||
|
return step.stepId === AutomationActionStepId.FILTER
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLoopStep(
|
||||||
|
step: AutomationStep | AutomationTrigger
|
||||||
|
): step is LoopStep {
|
||||||
|
return step.stepId === AutomationActionStepId.LOOP
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isActionStep(
|
||||||
|
step: AutomationStep | AutomationTrigger
|
||||||
|
): step is AutomationStep {
|
||||||
|
return step.type === AutomationStepType.ACTION
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCronTrigger(
|
||||||
|
trigger: AutomationStep | AutomationTrigger
|
||||||
|
): trigger is CronTrigger {
|
||||||
|
return trigger.stepId === AutomationTriggerStepId.CRON
|
||||||
|
}
|
||||||
|
|
||||||
type EmptyInputs = {}
|
type EmptyInputs = {}
|
||||||
export type AutomationStepDefinition = Omit<AutomationStep, "id" | "inputs"> & {
|
export type AutomationStepDefinition = Omit<AutomationStep, "id" | "inputs"> & {
|
||||||
inputs: EmptyInputs
|
inputs: EmptyInputs
|
||||||
|
@ -399,11 +425,13 @@ export type AutomationTriggerDefinition = Omit<
|
||||||
|
|
||||||
export type AutomationTriggerInputs<T extends AutomationTriggerStepId> =
|
export type AutomationTriggerInputs<T extends AutomationTriggerStepId> =
|
||||||
T extends AutomationTriggerStepId.APP
|
T extends AutomationTriggerStepId.APP
|
||||||
? void | Record<string, any>
|
?
|
||||||
|
| void
|
||||||
|
| (Record<string, any> & { fields?: Record<string, AutomationIOType> })
|
||||||
: T extends AutomationTriggerStepId.CRON
|
: T extends AutomationTriggerStepId.CRON
|
||||||
? CronTriggerInputs
|
? CronTriggerInputs
|
||||||
: T extends AutomationTriggerStepId.ROW_ACTION
|
: T extends AutomationTriggerStepId.ROW_ACTION
|
||||||
? Record<string, any>
|
? RowActionTriggerInputs
|
||||||
: T extends AutomationTriggerStepId.ROW_DELETED
|
: T extends AutomationTriggerStepId.ROW_DELETED
|
||||||
? RowDeletedTriggerInputs
|
? RowDeletedTriggerInputs
|
||||||
: T extends AutomationTriggerStepId.ROW_SAVED
|
: T extends AutomationTriggerStepId.ROW_SAVED
|
||||||
|
@ -439,7 +467,7 @@ export interface AutomationTriggerSchema<
|
||||||
event?: AutomationEventType
|
event?: AutomationEventType
|
||||||
cronJobId?: string
|
cronJobId?: string
|
||||||
stepId: TTrigger
|
stepId: TTrigger
|
||||||
inputs: AutomationTriggerInputs<TTrigger> & Record<string, any> // The record union to be removed once the types are fixed
|
inputs: AutomationTriggerInputs<AutomationTriggerStepId>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AutomationTrigger =
|
export type AutomationTrigger =
|
||||||
|
|
|
@ -3,15 +3,28 @@ import {
|
||||||
GetAutomationTriggerDefinitionsResponse,
|
GetAutomationTriggerDefinitionsResponse,
|
||||||
} from "../../api"
|
} from "../../api"
|
||||||
|
|
||||||
export interface BranchPath {
|
export interface BlockPath {
|
||||||
stepIdx: number
|
stepIdx: number
|
||||||
branchIdx: number
|
branchIdx: number
|
||||||
branchStepId: string
|
branchStepId: string
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BlockDefinitions {
|
export interface BlockRef {
|
||||||
TRIGGER: Partial<GetAutomationTriggerDefinitionsResponse>
|
id: string
|
||||||
CREATABLE_TRIGGER: Partial<GetAutomationTriggerDefinitionsResponse>
|
looped?: string
|
||||||
ACTION: Partial<GetAutomationActionDefinitionsResponse>
|
pathTo: BlockPath[]
|
||||||
|
terminating?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BlockDefinitionTypes {
|
||||||
|
TRIGGER = "TRIGGER",
|
||||||
|
CREATABLE_TRIGGER = "CREATABLE_TRIGGER",
|
||||||
|
ACTION = "ACTION",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlockDefinitions {
|
||||||
|
[BlockDefinitionTypes.TRIGGER]: Partial<GetAutomationTriggerDefinitionsResponse>
|
||||||
|
[BlockDefinitionTypes.CREATABLE_TRIGGER]: Partial<GetAutomationTriggerDefinitionsResponse>
|
||||||
|
[BlockDefinitionTypes.ACTION]: Partial<GetAutomationActionDefinitionsResponse>
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue