Merge branch 'master' into BUDI-9127/remove-feature-flag

This commit is contained in:
Adria Navarro 2025-03-31 15:50:52 +02:00 committed by GitHub
commit c1a89fc554
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 4119 additions and 861 deletions

View File

@ -35,7 +35,7 @@
export let footer: string | undefined = undefined
export let open: boolean = false
export let searchTerm: string | undefined = undefined
export let loading: boolean | undefined = undefined
export let loading: boolean | undefined = false
export let onOptionMouseenter = () => {}
export let onOptionMouseleave = () => {}
export let customPopoverHeight: string | undefined = undefined

View File

@ -38,6 +38,7 @@
export let compare: any = undefined
export let onOptionMouseenter = () => {}
export let onOptionMouseleave = () => {}
export let loading: boolean | undefined = false
const dispatch = createEventDispatcher()
const onChange = (e: CustomEvent<any>) => {
@ -56,6 +57,7 @@
<Field {helpText} {label} {labelPosition} {error} {tooltip}>
<Select
{quiet}
{loading}
{disabled}
{readonly}
{value}

View File

@ -10,7 +10,7 @@
export let color: string | undefined = undefined
export let hoverColor: 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 tooltipColor: string | undefined = undefined
export let tooltipWrap: boolean = true

9
packages/builder/src/assets.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare module "*.png" {
const value: string
export default value
}
declare module "*.svg" {
const value: string
export default value
}

View File

@ -134,9 +134,6 @@
// Size of the view port
let viewDims = {}
// Edge around the draggable content
let contentDragPadding = 200
// Auto scroll
let scrollInterval
@ -234,7 +231,7 @@
: {}),
}))
offsetX = offsetX - xBump
offsetX -= xBump
} else if (e.ctrlKey || e.metaKey) {
// Scale the content on scrolling
let updatedScale
@ -261,7 +258,7 @@
}
: {}),
}))
offsetY = offsetY - yBump
offsetY -= yBump
}
}
@ -335,8 +332,8 @@
scrollX: state.scrollX + xInterval,
scrollY: state.scrollY + yInterval,
}))
offsetX = offsetX + xInterval
offsetY = offsetY + yInterval
offsetX += xInterval
offsetY += yInterval
}
}
@ -488,22 +485,35 @@
const viewToFocusEle = () => {
if ($focusElement) {
const viewWidth = viewDims.width
const viewHeight = viewDims.height
// 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.
const targetX =
contentWrap.getBoundingClientRect().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
// Shift the content up slightly to accommodate the padding
contentPos.update(state => ({
...state,
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"
on:mouseup={onMouseUp}
on:mousemove={Utils.domDebounce(onMouseMove)}
style={`--dragPadding: ${contentDragPadding}px;`}
>
<svg class="draggable-background" style={`--dotSize: ${dotSize};`}>
<!-- Small 2px offset to tuck the points under the viewport on load-->
@ -617,7 +626,6 @@
transform-origin: 50% 50%;
transform: scale(var(--scale));
user-select: none;
padding: var(--dragPadding);
}
.content-wrap {

View File

@ -9,7 +9,7 @@
Tags,
Tag,
} from "@budibase/bbui"
import { AutomationActionStepId } from "@budibase/types"
import { AutomationActionStepId, BlockDefinitionTypes } from "@budibase/types"
import { automationStore, selectedAutomation } from "@/stores/builder"
import { admin, licensing } from "@/stores/portal"
import { externalActions } from "./ExternalActions"
@ -124,15 +124,20 @@
try {
const newBlock = automationStore.actions.constructBlock(
"ACTION",
BlockDefinitionTypes.ACTION,
action.stepId,
action
)
await automationStore.actions.addBlockToAutomation(
newBlock,
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()
} catch (error) {
console.error(error)
@ -167,8 +172,8 @@
<img
width={20}
height={20}
src={getExternalAction(action.stepId).icon}
alt={getExternalAction(action.stepId).name}
src={getExternalAction(action.stepId)?.icon}
alt={getExternalAction(action.stepId)?.name}
/>
<span class="icon-spacing">
<Body size="XS">

View File

@ -3,30 +3,28 @@
import {
Drawer,
DrawerContent,
ActionButton,
Icon,
Layout,
Body,
Divider,
TooltipPosition,
TooltipType,
Button,
Modal,
ModalContent,
} from "@budibase/bbui"
import PropField from "@/components/automation/SetupPanel/PropField.svelte"
import AutomationBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte"
import FlowItemHeader from "./FlowItemHeader.svelte"
import FlowItemActions from "./FlowItemActions.svelte"
import FlowItemStatus from "./FlowItemStatus.svelte"
import {
automationStore,
selectedAutomation,
evaluationContext,
contextMenuStore,
} from "@/stores/builder"
import { QueryUtils, Utils, memo } from "@budibase/frontend-core"
import { cloneDeep } from "lodash/fp"
import { createEventDispatcher, getContext } from "svelte"
import DragZone from "./DragZone.svelte"
import BlockHeader from "../../SetupPanel/BlockHeader.svelte"
const dispatch = createEventDispatcher()
@ -41,7 +39,6 @@
const memoContext = memo({})
let drawer
let open = true
let confirmDeleteModal
$: memoContext.set($evaluationContext)
@ -61,6 +58,65 @@
branchNode: true,
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>
<Modal bind:this={confirmDeleteModal}>
@ -112,7 +168,7 @@
</DrawerContent>
</Drawer>
<div class="flow-item">
<div class="flow-item branch">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class={`block branch-node hoverable`}
@ -121,96 +177,64 @@
e.stopPropagation()
}}
>
<FlowItemHeader
{automation}
{open}
itemName={branch.name}
block={step}
deleteStep={async () => {
const branchSteps = step.inputs?.children[branch.id]
if (branchSteps.length) {
confirmDeleteModal.show()
} else {
await automationStore.actions.deleteBranch(
branchBlockRef.pathTo,
$selectedAutomation.data
)
}
}}
on:update={async e => {
let stepUpdate = cloneDeep(step)
let branchUpdate = stepUpdate.inputs?.branches.find(
stepBranch => stepBranch.id == branch.id
)
branchUpdate.name = e.detail
<div class="block-float">
<FlowItemStatus
block={step}
{automation}
{branch}
hideStatus={$view?.dragging}
/>
</div>
<div class="blockSection">
<div class="heading">
<BlockHeader
{automation}
block={step}
itemName={branch.name}
on:update={async e => {
let stepUpdate = cloneDeep(step)
let branchUpdate = stepUpdate.inputs?.branches.find(
stepBranch => stepBranch.id == branch.id
)
branchUpdate.name = e.detail
const updatedAuto = automationStore.actions.updateStep(
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,
const updatedAuto = automationStore.actions.updateStep(
pathTo,
$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>
</FlowItemHeader>
{#if open}
<Divider noMargin />
<div class="blockSection">
<!-- Content body for possible slot -->
<Layout noPadding>
<PropField label="Only run when">
<ActionButton fullWidth on:click={drawer.show}>
{editableConditionUI?.groups?.length
? "Update condition"
: "Add condition"}
</ActionButton>
</PropField>
<div class="footer">
<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>
<Divider noMargin />
<div class="blockSection filter-button">
<PropField label="Only run when:" fullWidth>
<div style="width: 100%">
<Button secondary on:click={drawer.show}>
{editableConditionUI?.groups?.length
? "Update condition"
: "Add condition"}
</Button>
</div>
</PropField>
</div>
</div>
<div class="separator" />
@ -227,6 +251,9 @@
</div>
<style>
.filter-button :global(.spectrum-Button) {
width: 100%;
}
.branch-actions {
display: flex;
gap: var(--spacing-l);
@ -264,7 +291,7 @@
display: inline-block;
}
.block {
width: 480px;
width: 360px;
font-size: 16px;
background-color: var(--background);
border: 1px solid var(--spectrum-global-color-gray-300);
@ -289,4 +316,30 @@
align-items: center;
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>

View File

@ -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 },
}

View File

@ -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 },
}

View File

@ -3,6 +3,7 @@
automationStore,
automationHistoryStore,
selectedAutomation,
appStore,
} from "@/stores/builder"
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
import TestDataModal from "./TestDataModal.svelte"
@ -19,6 +20,9 @@
import { memo } from "@budibase/frontend-core"
import { sdk } from "@budibase/shared-core"
import DraggableCanvas from "../DraggableCanvas.svelte"
import { onMount } from "svelte"
import { environment } from "@/stores/portal"
import Count from "../../SetupPanel/Count.svelte"
export let automation
@ -31,6 +35,10 @@
let treeEle
let draggable
let prodErrors
$: $automationStore.showTestModal === true && testDataModal.show()
// Memo auto - selectedAutomation
$: memoAutomation.set(automation)
@ -63,53 +71,65 @@
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>
<div class="header" class:scrolling>
<div class="header-left">
<UndoRedoControl store={automationHistoryStore} showButtonGroup />
<div class="zoom">
<div class="group">
<ActionButton icon="Add" quiet on:click={draggable.zoomIn} />
<ActionButton icon="Remove" quiet on:click={draggable.zoomOut} />
</div>
<div class="automation-heading">
<div class="actions-left">
<div class="automation-name">
{automation.name}
</div>
<Button
secondary
on:click={() => {
draggable.zoomToFit()
}}
>
Zoom to fit
</Button>
</div>
<div class="controls">
<Button
icon={"Play"}
cta
<div class="actions-right">
<ActionButton
icon="Play"
quiet
disabled={!automation?.definition?.trigger}
on:click={() => {
testDataModal.show()
automationStore.update(state => ({ ...state, showTestModal: true }))
}}
>
Run test
</Button>
<div class="buttons">
{#if !$automationStore.showTestPanel && $automationStore.testResults}
<Button
secondary
icon={"Multiple"}
disabled={!$automationStore.testResults}
on:click={() => {
$automationStore.showTestPanel = true
}}
>
Test details
</Button>
{/if}
</div>
</ActionButton>
<Count
count={prodErrors}
tooltip={"There are errors in production"}
hoverable={false}
>
<ActionButton
icon="Folder"
quiet
selected={prodErrors}
on:click={() => {
const params = new URLSearchParams({
...(prodErrors ? { open: "error" } : {}),
automationId: automation._id,
})
window.open(
`/builder/app/${
$appStore.appId
}/settings/automations?${params.toString()}`,
"_blank"
)
}}
>
Logs
</ActionButton>
</Count>
{#if !isRowAction}
<div class="toggle-active setting-spacing">
<Toggle
@ -126,33 +146,59 @@
</div>
</div>
<div class="root" bind:this={treeEle}>
<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 class="main-flow">
<div class="canvas-heading" class:scrolling>
<div class="canvas-controls">
<div class="canvas-heading-left">
<UndoRedoControl store={automationHistoryStore} showButtonGroup />
<div class="zoom">
<div class="group">
<ActionButton icon="Add" quiet on:click={draggable.zoomIn} />
<ActionButton icon="Remove" quiet on:click={draggable.zoomOut} />
</div>
</div>
<Button
secondary
on:click={() => {
draggable.zoomToFit()
}}
>
Zoom to fit
</Button>
</div>
</div>
</div>
<div class="root" bind:this={treeEle}>
<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>
<ConfirmDialog
@ -166,11 +212,50 @@
This action cannot be undone.
</ConfirmDialog>
<Modal bind:this={testDataModal} width="30%" zIndex={5}>
<Modal
bind:this={testDataModal}
zIndex={5}
on:hide={() => {
automationStore.update(state => ({ ...state, showTestModal: false }))
}}
>
<TestDataModal />
</Modal>
<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) {
margin: 0px;
}
@ -181,12 +266,12 @@
align-items: center;
}
.header-left {
.canvas-heading-left {
display: flex;
gap: var(--spacing-l);
}
.header-left :global(div) {
.canvas-heading-left :global(div) {
border-right: none;
}
@ -207,50 +292,31 @@
box-sizing: border-box;
}
.header.scrolling {
.canvas-heading.scrolling {
background: var(--background);
border-bottom: var(--border-light);
z-index: 1;
}
.header {
z-index: 1;
.canvas-controls {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-l);
flex: 0 0 60px;
padding-right: var(--spacing-xl);
position: absolute;
width: 100%;
box-sizing: border-box;
pointer-events: none;
}
.header > * {
.canvas-controls > * {
pointer-events: auto;
}
.controls {
display: flex;
gap: var(--spacing-l);
}
.controls .toggle-active :global(.spectrum-Switch-label) {
.toggle-active :global(.spectrum-Switch-label) {
margin-right: 0px;
}
.buttons {
display: flex;
justify-content: flex-end;
align-items: center;
gap: var(--spacing-s);
}
.buttons:hover {
cursor: pointer;
}
.group {
border-radius: 4px;
display: flex;
@ -266,17 +332,17 @@
border-bottom-right-radius: 0;
}
.header-left .group :global(.spectrum-Button),
.header-left .group :global(.spectrum-ActionButton),
.header-left .group :global(.spectrum-Icon) {
.canvas-heading-left .group :global(.spectrum-Button),
.canvas-heading-left .group :global(.spectrum-ActionButton),
.canvas-heading-left .group :global(.spectrum-Icon) {
color: var(--spectrum-global-color-gray-900) !important;
}
.header-left .group :global(.spectrum-Button),
.header-left .group :global(.spectrum-ActionButton) {
.canvas-heading-left .group :global(.spectrum-Button),
.canvas-heading-left .group :global(.spectrum-ActionButton) {
background: var(--spectrum-global-color-gray-200) !important;
}
.header-left .group :global(.spectrum-Button:hover),
.header-left .group :global(.spectrum-ActionButton:hover) {
.canvas-heading-left .group :global(.spectrum-Button:hover),
.canvas-heading-left .group :global(.spectrum-ActionButton:hover) {
background: var(--spectrum-global-color-gray-300) !important;
}
</style>

View File

@ -1,38 +1,21 @@
<script>
import {
automationStore,
permissions,
selectedAutomation,
tables,
} from "@/stores/builder"
import {
Icon,
Divider,
Layout,
Detail,
Modal,
Label,
AbsTooltip,
} from "@budibase/bbui"
import { automationStore, selectedAutomation, tables } from "@/stores/builder"
import { Modal } from "@budibase/bbui"
import { sdk } from "@budibase/shared-core"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
import FlowItemHeader from "./FlowItemHeader.svelte"
import RoleSelect from "@/components/design/settings/controls/RoleSelect.svelte"
import { ActionStepID, TriggerStepID } from "@/constants/backend/automations"
import { ActionStepID } from "@/constants/backend/automations"
import { AutomationStepType } from "@budibase/types"
import FlowItemActions from "./FlowItemActions.svelte"
import FlowItemStatus from "./FlowItemStatus.svelte"
import DragHandle from "@/components/design/settings/controls/DraggableList/drag-handle.svelte"
import { getContext } from "svelte"
import DragZone from "./DragZone.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 blockRef
export let testDataModal
export let idx
export let automation
export let bindings
export let draggable = true
const view = getContext("draggableView")
@ -40,13 +23,47 @@
const contentPos = getContext("contentPos")
let webhookModal
let open = true
let showLooping = false
let role
let blockEle
let positionStyles
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 = () => {
if (!blockEle) {
return
@ -66,48 +83,8 @@
: []
}
$: pathSteps = loadSteps(blockRef)
$: 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) {
const move = (block, dragPos, dragging, scrollX, scrollY) => {
if ((!block && !dragging) || !dragPos) {
return
}
positionStyles = `
@ -125,52 +102,6 @@
--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 => {
if (isTrigger) {
e.preventDefault()
@ -205,143 +136,58 @@
<div
id={`block-${block.id}`}
class={`block ${block.type} hoverable`}
class:selected
class:dragging
class:draggable
class:selected
>
<div class="wrap">
{#if $view.dragging && selected}
{#if $view.dragging && dragging}
<div class="drag-placeholder" style={placeholderDims} />
{/if}
<div
bind:this={blockEle}
class="block-content"
class:dragging={$view.dragging && selected}
class:dragging={$view.dragging && dragging}
style={positionStyles}
on:mousedown={e => {
e.stopPropagation()
}}
>
<div class="block-float">
<FlowItemStatus {block} {automation} hideStatus={$view?.dragging} />
</div>
{#if draggable}
<div
class="handle"
class:grabbing={selected}
class:grabbing={dragging}
on:mousedown={onHandleMouseDown}
>
<DragHandle />
</div>
{/if}
<div class="block-core">
{#if loopBlock}
<div
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
on:click={() => {
showLooping = !showLooping
}}
class="splitHeader"
>
<div class="center-items">
<svg
width="28px"
height="28px"
class="spectrum-Icon"
style="color:var(--spectrum-global-color-gray-700);"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Reuse" />
</svg>
<div class="iconAlign">
<Detail size="S">Looping</Detail>
</div>
</div>
<div class="blockTitle">
<AbsTooltip type="negative" text="Remove looping">
<Icon
on:click={removeLooping}
hoverable
name="DeleteOutline"
/>
</AbsTooltip>
<div style="margin-left: 10px;" on:click={() => {}}>
<Icon
hoverable
name={showLooping ? "ChevronDown" : "ChevronUp"}
/>
</div>
</div>
</div>
</div>
<Divider noMargin />
{#if !showLooping}
<div class="blockSection">
<Layout noPadding gap="S">
<AutomationBlockSetup
schemaProperties={Object.entries(
$automationStore.blockDefinitions.ACTION.LOOP.schema
.inputs.properties
)}
{webhookModal}
block={loopBlock}
{automation}
{bindings}
/>
</Layout>
</div>
<Divider noMargin />
{/if}
{/if}
<FlowItemHeader
{automation}
{open}
{block}
{testDataModal}
{idx}
{addLooping}
{deleteStep}
on:toggle={() => (open = !open)}
on:update={async e => {
const newName = e.detail
if (newName.length === 0) {
await automationStore.actions.deleteAutomationName(block.id)
} else {
await automationStore.actions.saveAutomationName(
block.id,
newName
)
}
}}
/>
{#if open}
<Divider noMargin />
<div class="blockSection">
<Layout noPadding gap="S">
{#if isAppAction}
<div>
<Label>Role</Label>
<RoleSelect bind:value={role} />
</div>
{/if}
<AutomationBlockSetup
schemaProperties={Object.entries(
block?.schema?.inputs?.properties || {}
)}
{block}
{webhookModal}
{automation}
{bindings}
/>
{#if isTrigger && triggerInfo}
<InfoDisplay
title={triggerInfo.title}
body="This trigger is tied to your '{triggerInfo.tableName}' table"
icon="InfoOutline"
/>
{/if}
</Layout>
<InfoDisplay
title={triggerInfo.title}
body="This trigger is tied to your '{triggerInfo.tableName}' table"
icon="InfoOutline"
/>
</div>
{/if}
</div>
@ -397,7 +243,7 @@
display: inline-block;
}
.block {
width: 480px;
width: 360px;
font-size: 16px;
border-radius: 4px;
cursor: default;
@ -419,6 +265,8 @@
padding: 6px;
color: var(--grey-6);
cursor: grab;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.block.draggable .wrap .handle.grabbing {
cursor: grabbing;
@ -469,4 +317,22 @@
top: var(--blockPosY);
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>

View File

@ -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>

View File

@ -46,16 +46,26 @@
...userBindings,
...settingBindings,
]
onMount(() => {
// Register the trigger as the focus element for the automation
// Onload, the canvas will use the dimensions to center the step
if (stepEle && step.type === "TRIGGER" && !$view.focusEle) {
const { width, height, left, right, top, bottom, x, y } =
stepEle.getBoundingClientRect()
view.update(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 } : {}),
},
}))
}
})

View File

@ -122,7 +122,6 @@
await tick()
try {
await automationStore.actions.test($selectedAutomation.data, testData)
$automationStore.showTestPanel = true
} catch (error) {
notifications.error(error)
}

View File

@ -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>

View File

@ -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}

View File

@ -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}

View File

@ -1,15 +1,18 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { Select } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { automationStore, selectedAutomation } from "@/stores/builder"
import { TriggerStepID } from "@/constants/backend/automations"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import PropField from "./PropField.svelte"
const dispatch = createEventDispatcher()
export let value
export let bindings = []
export let title
const onChangeAutomation = e => {
value.automationId = e.detail
dispatch("change", value)
@ -33,9 +36,8 @@
)
</script>
<div class="schema-field">
<Label>Automation</Label>
<div class="field-width">
<div class="selector">
<PropField label={title} fullWidth>
<Select
on:change={onChangeAutomation}
value={value.automationId}
@ -43,13 +45,12 @@
getOptionValue={automation => automation._id}
getOptionLabel={automation => automation.name}
/>
</div>
</PropField>
</div>
{#if Object.keys(automationFields)}
{#each Object.keys(automationFields) as field}
<div class="schema-field">
<Label>{field}</Label>
<div class="field-width">
<PropField label={field} fullWidth>
<DrawerBindableInput
panel={AutomationBindingPanel}
extraThin
@ -59,27 +60,12 @@
{bindings}
updateOnChange={false}
/>
</div>
</PropField>
</div>
{/each}
{/if}
<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) {
text-transform: capitalize;
}

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,10 +1,10 @@
<script>
<script lang="ts">
import { Label } from "@budibase/bbui"
export let label
export let labelTooltip
export let fullWidth = false
export let componentWidth = 320
export let label: string | undefined = ""
export let labelTooltip: string | undefined = ""
export let fullWidth: boolean | undefined = false
export let componentWidth: number | undefined = 320
</script>
<div

View File

@ -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}
/>

View File

@ -11,8 +11,6 @@
import { FieldType } from "@budibase/types"
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 { capitalise } from "@/helpers"
import { memo } from "@budibase/frontend-core"
@ -26,6 +24,8 @@
export let bindings
export let isTestModal
export let context = {}
export let componentWidth
export let fullWidth = false
const typeToField = Object.values(FIELDS).reduce((acc, field) => {
acc[field.type] = field
@ -234,66 +234,28 @@
)
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>
{#each schemaFields || [] as [field, schema]}
{#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">
{#if isTestModal}
<RowSelectorTypes
{isTestModal}
{field}
{schema}
bindings={parsedBindings}
value={editableRow}
meta={{
fields: editableFields,
}}
{onChange}
{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}
<RowSelectorTypes
{isTestModal}
{field}
{schema}
bindings={parsedBindings}
value={editableRow}
meta={{
fields: editableFields,
}}
{onChange}
{context}
/>
</div>
</PropField>
{/if}
@ -305,24 +267,30 @@
class:empty={Object.is(editableFields, {})}
bind:this={popoverAnchor}
>
<ActionButton
icon="Add"
on:click={() => {
customPopover.show()
}}
disabled={!schemaFields}
>Add fields
</ActionButton>
<ActionButton
icon="Remove"
on:click={() => {
dispatch("change", {
meta: { fields: {} },
row: {},
})
}}
>Clear
</ActionButton>
<PropField {componentWidth} {fullWidth}>
<div class="prop-control-wrap">
<ActionButton
on:click={() => {
customPopover.show()
}}
disabled={!schemaFields}
>
Edit fields
</ActionButton>
{#if schemaFields.length}
<ActionButton
on:click={() => {
dispatch("change", {
meta: { fields: {} },
row: {},
})
}}
>
Clear
</ActionButton>
{/if}
</div>
</PropField>
</div>
{/if}
@ -388,11 +356,4 @@
.prop-control-wrap :global(.icon.json-slot-icon) {
right: 1px !important;
}
.add-fields-btn {
display: flex;
flex-direction: row;
justify-content: center;
gap: var(--spacing-s);
}
</style>

View File

@ -7,7 +7,7 @@
const dispatch = createEventDispatcher()
export let value
export let isTrigger
export let isTrigger = false
export let disabled = false
$: filteredTables = $tables.list.filter(table => {

View File

@ -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,
}

View File

@ -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
}
}

View File

@ -7,15 +7,15 @@
import { createEventDispatcher } from "svelte"
import { capitalise } from "@/helpers"
export let value
export let error
export let value = undefined
export let error = undefined
export let placeholder = null
export let autoWidth = false
export let quiet = false
export let allowPublic = true
export let allowRemove = false
export let disabled = false
export let align
export let align = undefined
export let footer = null
export let allowedRoles = null
export let allowCreator = false
@ -135,7 +135,6 @@
{autoWidth}
{quiet}
{disabled}
{align}
{footer}
bind:value
on:change={onChange}

View File

@ -49,11 +49,11 @@
export let bindings: EnrichedBinding[] = []
export let value: string = ""
export let allowHBS = true
export let allowJS = false
export let allowHBS: boolean | undefined = true
export let allowJS: boolean | undefined = false
export let allowHelpers = 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 autofocusEditor = false
export let placeholder: string | null = null
@ -128,7 +128,7 @@
}
}
const getModeOptions = (allowHBS: boolean, allowJS: boolean) => {
const getModeOptions = (allowHBS = true, allowJS = false) => {
let options = []
if (allowHBS) {
options.push(BindingMode.Text)

View File

@ -21,15 +21,15 @@
import SnippetDrawer from "./SnippetDrawer.svelte"
import UpgradeButton from "@/pages/builder/portal/_components/UpgradeButton.svelte"
export let addHelper: (_helper: Helper, _js?: boolean) => void
export let addBinding: (_binding: EnrichedBinding) => void
export let addSnippet: (_snippet: Snippet) => void
export let bindings: EnrichedBinding[]
export let snippets: Snippet[] | null
export let mode: BindingMode
export let allowHelpers: boolean
export let allowSnippets: boolean
export let context = null
export let addHelper: (_helper: Helper, _js?: boolean) => void = () => {}
export let addBinding: (_binding: EnrichedBinding) => void = () => {}
export let addSnippet: (_snippet: Snippet) => void = () => {}
export let bindings: EnrichedBinding[] | undefined
export let snippets: Snippet[] | null = null
export let mode: BindingMode | undefined = BindingMode.Text
export let allowHelpers: boolean = true
export let allowSnippets: boolean = true
export let context: Record<any, any> | undefined = undefined
let search = ""
let searching = false
@ -93,7 +93,7 @@
search
)
function onModeChange(_mode: BindingMode) {
function onModeChange(_mode: BindingMode | undefined) {
selectedCategory = null
}
$: onModeChange(mode)

View File

@ -36,10 +36,10 @@
export let value: string = ""
export let allowHelpers = true
export let allowSnippets = true
export let context = null
export let context: Record<any, any> | undefined = undefined
export let autofocusEditor = false
export let placeholder = null
export let height = 180
export let placeholder: string | undefined
export let dropdown = DropdownPosition.Absolute
let getCaretPosition: CaretPositionFn | undefined
let insertAtPos: InsertAtPositionFn | undefined
@ -126,26 +126,25 @@
}
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 }) => {
if (!e.detail?.trim()) {
// Don't bother saving empty values as JS
updateValue(null)
} else {
updateValue(encodeJSBinding(e.detail))
}
const onBlurJSValue = (e: { detail: string }) => {
// Don't bother saving empty values as JS
updateValue(e.detail?.trim())
}
</script>
<div class="code-panel" style="height:{height}px;">
<div class="code-panel">
<div class="editor">
{#key jsCompletions}
<CodeEditor
value={jsValue || ""}
on:change={onChangeJSValue}
on:blur
on:blur={onBlurJSValue}
completions={jsCompletions}
{bindings}
mode={EditorModes.JS}
@ -155,7 +154,7 @@
placeholder={placeholder ||
"Add bindings by typing $ or use the menu on the right"}
jsBindingWrapping
dropdown={DropdownPosition.Absolute}
{dropdown}
/>
{/key}
</div>
@ -164,6 +163,7 @@
<style>
.code-panel {
display: flex;
height: 100%;
}
/* Editor */

View File

@ -14,14 +14,15 @@
export let value = ""
export let bindings = []
export let title = "Bindings"
export let placeholder
export let label
export let placeholder = undefined
export let label = undefined
export let disabled = false
export let allowJS = true
export let allowHelpers = true
export let updateOnChange = true
export let type
export let schema
export let type = undefined
export let schema = undefined
export let allowHBS = true
export let context = {}
@ -230,7 +231,6 @@
}
.icon {
right: 1px;
bottom: 1px;
position: absolute;
justify-content: center;
@ -239,8 +239,6 @@
flex-direction: row;
box-sizing: border-box;
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;
color: var(--spectrum-alias-text-color);
background-color: var(--spectrum-global-color-gray-75);

View File

@ -7,15 +7,15 @@
readableToRuntimeBinding,
} from "@/dataBinding"
export let schemaFields
export let filters
export let schemaFields = undefined
export let filters = undefined
export let bindings = []
export let panel = ClientBindingPanel
export let allowBindings = true
export let allowOnEmpty
export let datasource
export let builderType
export let docsURL
export let allowOnEmpty = undefined
export let datasource = undefined
export let builderType = undefined
export let docsURL = undefined
export let evaluationContext = {}
</script>

View File

@ -15,24 +15,23 @@
const dispatch = createEventDispatcher()
export let defaults
export let defaults = undefined
export let object = defaults || {}
export let activity = {}
export let readOnly
export let noAddButton
export let name
export let readOnly = false
export let noAddButton = false
export let name = ""
export let headings = false
export let options
export let toggle
export let options = undefined
export let toggle = false
export let keyPlaceholder = "Key"
export let valuePlaceholder = "Value"
export let valueHeading
export let keyHeading
export let tooltip
export let menuItems
export let valueHeading = ""
export let keyHeading = ""
export let tooltip = ""
export let menuItems = []
export let showMenu = false
export let bindings = []
export let bindingDrawerLeft
export let allowHelpers = true
export let customButtonText = null
export let keyBindings = false
@ -140,7 +139,6 @@
value={field.name}
{allowJS}
{allowHelpers}
drawerLeft={bindingDrawerLeft}
{context}
/>
{:else}
@ -167,7 +165,6 @@
value={field.value}
{allowJS}
{allowHelpers}
drawerLeft={bindingDrawerLeft}
{context}
/>
{:else}

View File

@ -21,7 +21,6 @@
keyPlaceholder="Binding name"
valuePlaceholder="Default"
bindings={[...userBindings]}
bindingDrawerLeft="260px"
allowHelpers={false}
on:change
/>

View File

@ -582,7 +582,6 @@
...globalDynamicRequestBindings,
...dataSourceStaticBindings,
]}
bindingDrawerLeft="260px"
/>
</Tab>
<Tab title="Params">
@ -591,7 +590,6 @@
name="param"
headings
bindings={mergedBindings}
bindingDrawerLeft="260px"
/>
</Tab>
<Tab title="Headers">
@ -602,7 +600,6 @@
name="header"
headings
bindings={mergedBindings}
bindingDrawerLeft="260px"
/>
</Tab>
<Tab title="Body">

View File

@ -3,8 +3,7 @@
import AutomationPanel from "@/components/automation/AutomationPanel/AutomationPanel.svelte"
import CreateAutomationModal from "@/components/automation/AutomationPanel/CreateAutomationModal.svelte"
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
import TestPanel from "@/components/automation/AutomationBuilder/TestPanel.svelte"
import { onDestroy, onMount } from "svelte"
import { onDestroy } from "svelte"
import { syncURLToState } from "@/helpers/urlStateSync"
import * as routify from "@roxi/routify"
import {
@ -12,8 +11,10 @@
automationStore,
selectedAutomation,
} from "@/stores/builder"
import StepPanel from "@/components/automation/AutomationBuilder/StepPanel.svelte"
$: automationId = $selectedAutomation?.data?._id
$: blockRefs = $selectedAutomation.blockRefs
$: builderStore.selectResource(automationId)
const stopSyncing = syncURLToState({
@ -29,15 +30,6 @@
let modal
let webhookModal
onMount(async () => {
await automationStore.actions.initAppSelf()
// Init the binding evaluation context
automationStore.actions.initContext()
$automationStore.showTestPanel = false
})
onDestroy(stopSyncing)
</script>
@ -70,15 +62,16 @@
{/if}
</div>
{#if $automationStore.showTestPanel}
<div class="setup">
<TestPanel automation={$selectedAutomation.data} />
{#if blockRefs[$automationStore.selectedNodeId] && $automationStore.selectedNodeId}
<div class="step-panel">
<StepPanel />
</div>
{/if}
<Modal bind:this={modal}>
<CreateAutomationModal {webhookModal} />
</Modal>
<Modal bind:this={webhookModal} width="30%">
<Modal bind:this={webhookModal}>
<CreateWebhookModal />
</Modal>
</div>
@ -115,16 +108,17 @@
.main {
width: 300px;
}
.setup {
padding-top: 9px;
.step-panel {
border-left: var(--border-light);
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-l);
background-color: var(--background);
grid-column: 3;
overflow: auto;
grid-column: 3;
width: 360px;
max-width: 360px;
}
</style>

View File

@ -147,6 +147,13 @@
await automationStore.actions.fetch()
const params = new URLSearchParams(window.location.search)
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) {
status = ERROR
}

File diff suppressed because it is too large Load Diff

View File

@ -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
}

View File

@ -1,6 +1,7 @@
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"lib": ["ESNext"],
"baseUrl": ".",

View File

@ -16,6 +16,9 @@ import {
FieldType,
FilterCondition,
isDidNotTriggerResponse,
RowActionTriggerInputs,
RowCreatedTriggerInputs,
RowDeletedTriggerInputs,
SettingsConfig,
Table,
} from "@budibase/types"
@ -259,7 +262,10 @@ describe("/automations", () => {
it("tests the automation successfully", async () => {
let table = await config.createTable()
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 = {
row: {
name: "{{trigger.row.name}}",
@ -487,22 +493,28 @@ describe("/automations", () => {
.serverLog({ text: "test" })
.save()
automation.definition.trigger.inputs.tableId = "newTableId"
const triggerInputs = automation.definition.trigger
.inputs as RowDeletedTriggerInputs
triggerInputs.tableId = "newTableId"
const { automation: updatedAutomation } =
await config.api.automation.update(automation)
expect(updatedAutomation.definition.trigger.inputs.tableId).toEqual(
"newTableId"
)
const updatedTriggerInputs = updatedAutomation.definition.trigger
.inputs as RowDeletedTriggerInputs
expect(updatedTriggerInputs.tableId).toEqual("newTableId")
})
it("cannot update a readonly field", async () => {
const { automation } = await createAutomationBuilder(config)
.onRowAction({ tableId: "tableId" })
.onRowAction({ tableId: "tableId", rowActionId: "someId" })
.serverLog({ text: "test" })
.save()
automation.definition.trigger.inputs.tableId = "newTableId"
const triggerInputs = automation.definition.trigger
.inputs as RowActionTriggerInputs
triggerInputs.tableId = "newTableId"
await config.api.automation.update(automation, {
status: 400,
body: {

View File

@ -1,7 +1,7 @@
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import { captureAutomationResults } from "../utilities"
import { Automation } from "@budibase/types"
import { Automation, AutomationIOType } from "@budibase/types"
describe("app action trigger", () => {
const config = new TestConfiguration()
@ -48,7 +48,11 @@ describe("app action trigger", () => {
it("should correct coerce values based on the schema", async () => {
const { automation } = await createAutomationBuilder(config)
.onAppAction({
fields: { text: "string", number: "number", boolean: "boolean" },
fields: {
text: AutomationIOType.STRING,
number: AutomationIOType.NUMBER,
boolean: AutomationIOType.BOOLEAN,
},
})
.serverLog({
text: "{{ fields.text }} {{ fields.number }} {{ fields.boolean }}",

View File

@ -23,6 +23,8 @@ import {
AutomationResults,
DidNotTriggerResponse,
Table,
AutomationTriggerStepId,
AutomationTriggerInputs,
} from "@budibase/types"
import { executeInThread } from "../threads/automation"
import { dataFilters, sdk } from "@budibase/shared-core"
@ -35,6 +37,26 @@ const JOB_OPTS = {
import * as automationUtils from "../automations/automationUtils"
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() {
const db = context.getAppDB()
let automations = await db.allDocs<Automation>(
@ -64,11 +86,14 @@ async function queueRelevantRowAutomations(
// make sure it is the correct table ID as well
automations = automations.filter(automation => {
const trigger = automation.definition.trigger
const triggerInputs = trigger?.inputs as RowTriggerInputs
return (
trigger &&
trigger.event === eventType &&
!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
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 || {})) {
coercedFields[key] = coerce(params.fields[key], fields[key])
}
@ -269,8 +296,9 @@ async function checkTriggerFilters(
event: { row: Row; oldRow: Row }
): Promise<boolean> {
const trigger = automation.definition.trigger
const filters = trigger?.inputs?.filters
const tableId = trigger?.inputs?.tableId
const triggerInputs = trigger.inputs as RowFilterInputs
const filters = triggerInputs?.filters
const tableId = triggerInputs?.tableId
if (!filters) {
return true

View File

@ -1,17 +1,21 @@
import { Thread, ThreadType } from "../threads"
import { automations } from "@budibase/shared-core"
import { automationQueue } from "./bullboard"
import { updateEntityMetadata } from "../utilities"
import { context, db as dbCore, utils } from "@budibase/backend-core"
import { getAutomationMetadataParams } from "../db/utils"
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 { helpers, REBOOT_CRON } from "@budibase/shared-core"
import tracer from "dd-trace"
import { JobId } from "bull"
const CRON_STEP_ID = automations.triggers.definitions.CRON.stepId
let Runner: Thread
if (automationsEnabled()) {
Runner = new Thread(ThreadType.AUTOMATION)
@ -60,7 +64,11 @@ export async function processEvent(job: AutomationJob) {
const task = async () => {
try {
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
job.data.event.timestamp = Date.now()
}
@ -147,17 +155,14 @@ export async function clearMetadata() {
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) {
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
if (
isCronTrigger(automation) &&
trigger &&
isCronTrigger(trigger) &&
!isRebootTrigger(automation) &&
!automation.disabled &&
trigger?.inputs.cron
!automation.disabled
) {
const cronExp = trigger.inputs.cron
const inputs = trigger.inputs as CronTriggerInputs
const cronExp = inputs.cron
const validation = helpers.cron.validate(cronExp)
if (!validation.valid) {
throw new Error(

View File

@ -40,7 +40,7 @@ function cleanAutomationInputs(automation: Automation) {
if (step == null) {
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
if (!step.inputs[inputName] || step.inputs[inputName] === "") {
delete step.inputs[inputName]

View File

@ -6,6 +6,7 @@ import {
AutomationTriggerStepId,
RowAttachment,
FieldType,
WebhookTriggerInputs,
} from "@budibase/types"
import { getAutomationParams } from "../../../db/utils"
import { budibaseTempDir } from "../../../utilities/budibaseDir"
@ -99,7 +100,7 @@ async function updateAutomations(prodAppId: string, db: Database) {
if (
automation.definition.trigger?.stepId === AutomationTriggerStepId.WEBHOOK
) {
const old = automation.definition.trigger.inputs
const old = automation.definition.trigger.inputs as WebhookTriggerInputs
automation.definition.trigger.inputs = {
schemaUrl: old.schemaUrl.replace(oldDevAppId, devAppId),
triggerUrl: old.triggerUrl.replace(oldProdAppId, prodAppId),

View File

@ -28,7 +28,7 @@ export const definition: AutomationStepDefinition = {
},
},
customType: AutomationCustomIOType.AUTOMATION_FIELDS,
title: "automatioFields",
title: "Automation Fields",
required: ["automationId"],
},
timeout: {

View File

@ -320,6 +320,7 @@ export type WebhookTriggerOutputs = Record<string, any> & {
export type RowActionTriggerInputs = {
tableId: string
rowActionId: string
}
export type RowActionTriggerOutputs = {

View File

@ -148,20 +148,20 @@ export interface BaseIOStructure {
dependsOn?: string
enum?: string[]
pretty?: string[]
properties?: {
[key: string]: BaseIOStructure
}
properties?: AutomationIOProps
required?: string[]
readonly?: true
}
export interface InputOutputBlock {
properties: {
[key: string]: BaseIOStructure
}
properties: AutomationIOProps
required?: string[]
}
export interface AutomationIOProps {
[key: string]: BaseIOStructure
}
export enum AutomationFeature {
LOOPING = "LOOPING",
}

View File

@ -7,6 +7,7 @@ import {
InputOutputBlock,
AutomationTriggerStepId,
AutomationEventType,
AutomationIOType,
} from "./automation"
import {
CollectStepInputs,
@ -57,6 +58,7 @@ import {
RowCreatedTriggerOutputs,
RowUpdatedTriggerOutputs,
WebhookTriggerOutputs,
RowActionTriggerInputs,
} from "./StepInputsOutputs"
export type ActionImplementations<T extends Hosting> = {
@ -383,6 +385,30 @@ export function isAppTrigger(
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 = {}
export type AutomationStepDefinition = Omit<AutomationStep, "id" | "inputs"> & {
inputs: EmptyInputs
@ -399,11 +425,13 @@ export type AutomationTriggerDefinition = Omit<
export type AutomationTriggerInputs<T extends AutomationTriggerStepId> =
T extends AutomationTriggerStepId.APP
? void | Record<string, any>
?
| void
| (Record<string, any> & { fields?: Record<string, AutomationIOType> })
: T extends AutomationTriggerStepId.CRON
? CronTriggerInputs
: T extends AutomationTriggerStepId.ROW_ACTION
? Record<string, any>
? RowActionTriggerInputs
: T extends AutomationTriggerStepId.ROW_DELETED
? RowDeletedTriggerInputs
: T extends AutomationTriggerStepId.ROW_SAVED
@ -439,7 +467,7 @@ export interface AutomationTriggerSchema<
event?: AutomationEventType
cronJobId?: string
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 =

View File

@ -3,15 +3,28 @@ import {
GetAutomationTriggerDefinitionsResponse,
} from "../../api"
export interface BranchPath {
export interface BlockPath {
stepIdx: number
branchIdx: number
branchStepId: string
id: string
}
export interface BlockDefinitions {
TRIGGER: Partial<GetAutomationTriggerDefinitionsResponse>
CREATABLE_TRIGGER: Partial<GetAutomationTriggerDefinitionsResponse>
ACTION: Partial<GetAutomationActionDefinitionsResponse>
export interface BlockRef {
id: string
looped?: string
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>
}