Merge branch 'master' into BUDI-9127/remove-feature-flag
This commit is contained in:
commit
c1a89fc554
|
@ -35,7 +35,7 @@
|
|||
export let footer: string | undefined = undefined
|
||||
export let 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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
declare module "*.png" {
|
||||
const value: string
|
||||
export default value
|
||||
}
|
||||
|
||||
declare module "*.svg" {
|
||||
const value: string
|
||||
export default value
|
||||
}
|
|
@ -134,9 +134,6 @@
|
|||
// Size of the view port
|
||||
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 {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,22 +177,20 @@
|
|||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<FlowItemHeader
|
||||
{automation}
|
||||
{open}
|
||||
itemName={branch.name}
|
||||
<div class="block-float">
|
||||
<FlowItemStatus
|
||||
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
|
||||
)
|
||||
}
|
||||
}}
|
||||
{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(
|
||||
|
@ -151,66 +205,36 @@
|
|||
)
|
||||
await automationStore.actions.save(updatedAuto)
|
||||
}}
|
||||
on:toggle={() => (open = !open)}
|
||||
>
|
||||
<div slot="custom-actions" class="branch-actions">
|
||||
/>
|
||||
<div class="actions">
|
||||
<Icon
|
||||
on:click={() => {
|
||||
automationStore.actions.branchLeft(
|
||||
branchBlockRef.pathTo,
|
||||
$selectedAutomation.data,
|
||||
step
|
||||
)
|
||||
}}
|
||||
tooltip={"Move left"}
|
||||
tooltipType={TooltipType.Info}
|
||||
tooltipPosition={TooltipPosition.Top}
|
||||
hoverable
|
||||
disabled={branchIdx == 0}
|
||||
name="ArrowLeft"
|
||||
name="Info"
|
||||
tooltip="Branch sequencing checks each option in order and follows the first one that matches the rules."
|
||||
/>
|
||||
<Icon
|
||||
on:click={() => {
|
||||
automationStore.actions.branchRight(
|
||||
branchBlockRef.pathTo,
|
||||
$selectedAutomation.data,
|
||||
step
|
||||
)
|
||||
on:click={e => {
|
||||
openContextMenu(e)
|
||||
}}
|
||||
tooltip={"Move right"}
|
||||
tooltipType={TooltipType.Info}
|
||||
tooltipPosition={TooltipPosition.Top}
|
||||
size="S"
|
||||
hoverable
|
||||
disabled={isLast}
|
||||
name="ArrowRight"
|
||||
name="MoreSmallList"
|
||||
/>
|
||||
</div>
|
||||
</FlowItemHeader>
|
||||
{#if open}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider noMargin />
|
||||
<div class="blockSection">
|
||||
<!-- Content body for possible slot -->
|
||||
<Layout noPadding>
|
||||
<PropField label="Only run when">
|
||||
<ActionButton fullWidth on:click={drawer.show}>
|
||||
<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"}
|
||||
</ActionButton>
|
||||
</Button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import DiscordLogo from "assets/discord.svg"
|
||||
import ZapierLogo from "assets/zapier.png"
|
||||
import n8nLogo from "assets/n8n_square.png"
|
||||
import MakeLogo from "assets/make.svg"
|
||||
import SlackLogo from "assets/slack.svg"
|
||||
|
||||
export const externalActions = {
|
||||
zapier: { name: "zapier", icon: ZapierLogo },
|
||||
discord: { name: "discord", icon: DiscordLogo },
|
||||
slack: { name: "slack", icon: SlackLogo },
|
||||
integromat: { name: "integromat", icon: MakeLogo },
|
||||
n8n: { name: "n8n", icon: n8nLogo },
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import DiscordLogo from "assets/discord.svg"
|
||||
import ZapierLogo from "assets/zapier.png"
|
||||
import n8nLogo from "assets/n8n_square.png"
|
||||
import MakeLogo from "assets/make.svg"
|
||||
import SlackLogo from "assets/slack.svg"
|
||||
import { AutomationActionStepId } from "@budibase/types"
|
||||
|
||||
export type ExternalAction = {
|
||||
name: string
|
||||
icon: string
|
||||
}
|
||||
export const externalActions: Partial<
|
||||
Record<AutomationActionStepId, ExternalAction>
|
||||
> = {
|
||||
[AutomationActionStepId.zapier]: { name: "zapier", icon: ZapierLogo },
|
||||
[AutomationActionStepId.discord]: { name: "discord", icon: DiscordLogo },
|
||||
[AutomationActionStepId.slack]: { name: "slack", icon: SlackLogo },
|
||||
[AutomationActionStepId.integromat]: { name: "integromat", icon: MakeLogo },
|
||||
[AutomationActionStepId.n8n]: { name: "n8n", icon: n8nLogo },
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
automationStore,
|
||||
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,10 +71,85 @@
|
|||
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">
|
||||
<div class="automation-heading">
|
||||
<div class="actions-left">
|
||||
<div class="automation-name">
|
||||
{automation.name}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions-right">
|
||||
<ActionButton
|
||||
icon="Play"
|
||||
quiet
|
||||
disabled={!automation?.definition?.trigger}
|
||||
on:click={() => {
|
||||
automationStore.update(state => ({ ...state, showTestModal: true }))
|
||||
}}
|
||||
>
|
||||
Run test
|
||||
</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
|
||||
text={automation.disabled ? "Paused" : "Activated"}
|
||||
on:change={automationStore.actions.toggleDisabled(
|
||||
automation._id,
|
||||
automation.disabled
|
||||
)}
|
||||
disabled={!automation?.definition?.trigger}
|
||||
value={!automation.disabled}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
|
@ -85,44 +168,6 @@
|
|||
Zoom to fit
|
||||
</Button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<Button
|
||||
icon={"Play"}
|
||||
cta
|
||||
disabled={!automation?.definition?.trigger}
|
||||
on:click={() => {
|
||||
testDataModal.show()
|
||||
}}
|
||||
>
|
||||
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>
|
||||
{#if !isRowAction}
|
||||
<div class="toggle-active setting-spacing">
|
||||
<Toggle
|
||||
text={automation.disabled ? "Paused" : "Activated"}
|
||||
on:change={automationStore.actions.toggleDisabled(
|
||||
automation._id,
|
||||
automation.disabled
|
||||
)}
|
||||
disabled={!automation?.definition?.trigger}
|
||||
value={!automation.disabled}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -154,6 +199,7 @@
|
|||
</span>
|
||||
</DraggableCanvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
|
@ -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>
|
||||
|
|
|
@ -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="blockSection">
|
||||
<div
|
||||
on:click={() => {
|
||||
showLooping = !showLooping
|
||||
class="block-core"
|
||||
on:click={async () => {
|
||||
await automationStore.actions.selectNode(block.id)
|
||||
}}
|
||||
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}
|
||||
<div class="blockSection block-info">
|
||||
<BlockHeader
|
||||
disabled
|
||||
{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
|
||||
)
|
||||
}
|
||||
}}
|
||||
on:update={e =>
|
||||
automationStore.actions.updateBlockTitle(block, e.detail)}
|
||||
/>
|
||||
{#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}
|
||||
<div class="blockSection">
|
||||
<InfoDisplay
|
||||
title={triggerInfo.title}
|
||||
body="This trigger is tied to your '{triggerInfo.tableName}' table"
|
||||
icon="InfoOutline"
|
||||
/>
|
||||
{/if}
|
||||
</Layout>
|
||||
</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>
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
<script lang="ts">
|
||||
import { automationStore, selectedAutomation } from "@/stores/builder"
|
||||
import {
|
||||
type FlowItemStatus,
|
||||
DataMode,
|
||||
FilterableRowTriggers,
|
||||
FlowStatusType,
|
||||
} from "@/types/automations"
|
||||
import {
|
||||
type AutomationStep,
|
||||
type AutomationStepResult,
|
||||
type AutomationTrigger,
|
||||
type AutomationTriggerResult,
|
||||
type Branch,
|
||||
type DidNotTriggerResponse,
|
||||
type TestAutomationResponse,
|
||||
type AutomationTriggerStepId,
|
||||
isDidNotTriggerResponse,
|
||||
isTrigger,
|
||||
isBranchStep,
|
||||
isFilterStep,
|
||||
} from "@budibase/types"
|
||||
import { ActionButton } from "@budibase/bbui"
|
||||
|
||||
export let block: AutomationStep | AutomationTrigger | undefined
|
||||
export let branch: Branch | undefined
|
||||
export let hideStatus: boolean | undefined = false
|
||||
|
||||
$: blockRef = block?.id ? $selectedAutomation.blockRefs[block?.id] : null
|
||||
$: isTriggerBlock = block ? isTrigger(block) : false
|
||||
|
||||
$: testResults = $automationStore.testResults as TestAutomationResponse
|
||||
$: blockResult = automationStore.actions.processBlockResults(
|
||||
testResults,
|
||||
block
|
||||
)
|
||||
$: flowStatus = getFlowStatus(blockResult)
|
||||
|
||||
const getFlowStatus = (
|
||||
result?:
|
||||
| AutomationTriggerResult
|
||||
| AutomationStepResult
|
||||
| DidNotTriggerResponse
|
||||
): FlowItemStatus | undefined => {
|
||||
if (!result || !block) {
|
||||
return
|
||||
}
|
||||
const outputs = result?.outputs
|
||||
const isFilteredRowTrigger =
|
||||
isTriggerBlock &&
|
||||
isDidNotTriggerResponse(testResults) &&
|
||||
FilterableRowTriggers.includes(block.stepId as AutomationTriggerStepId)
|
||||
|
||||
if (
|
||||
isFilteredRowTrigger ||
|
||||
(isFilterStep(block) && "result" in outputs && outputs.result === false)
|
||||
) {
|
||||
return {
|
||||
message: "Stopped",
|
||||
icon: "Alert",
|
||||
type: FlowStatusType.WARN,
|
||||
}
|
||||
}
|
||||
|
||||
if (branch && isBranchStep(block)) {
|
||||
// Do not give status markers to branch nodes that were not part of the run.
|
||||
if ("branchId" in outputs && outputs.branchId !== branch.id) return
|
||||
|
||||
// Mark branches as stopped when no branch criteria was met
|
||||
if (outputs.success == false) {
|
||||
return {
|
||||
message: "Stopped",
|
||||
icon: "Alert",
|
||||
type: FlowStatusType.WARN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const success = outputs?.success || isTriggerBlock
|
||||
return {
|
||||
message: success ? "Completed" : "Failed",
|
||||
icon: success ? "CheckmarkCircle" : "AlertCircleFilled",
|
||||
type:
|
||||
success || isTriggerBlock
|
||||
? FlowStatusType.SUCCESS
|
||||
: FlowStatusType.ERROR,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flow-item-status">
|
||||
{#if blockRef}
|
||||
{#if isTriggerBlock}
|
||||
<span class="block-type">
|
||||
<ActionButton size="S" active={false} icon="Workflow">
|
||||
Trigger
|
||||
</ActionButton>
|
||||
</span>
|
||||
{:else if blockRef.looped}
|
||||
<ActionButton size="S" active={false} icon="Reuse">Looping</ActionButton>
|
||||
{:else}
|
||||
<span />
|
||||
{/if}
|
||||
{#if blockResult && flowStatus && !hideStatus}
|
||||
<span class={`flow-${flowStatus.type}`}>
|
||||
<ActionButton
|
||||
size="S"
|
||||
icon={flowStatus.icon}
|
||||
tooltip={flowStatus?.tooltip}
|
||||
on:click={async () => {
|
||||
if (branch || !block) {
|
||||
return
|
||||
}
|
||||
await automationStore.actions.selectNode(
|
||||
block?.id,
|
||||
flowStatus.type == FlowStatusType.SUCCESS
|
||||
? DataMode.OUTPUT
|
||||
: DataMode.ERRORS
|
||||
)
|
||||
}}
|
||||
>
|
||||
{flowStatus.message}
|
||||
</ActionButton>
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.flow-item-status {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-s);
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.flow-item-status :global(> *) {
|
||||
pointer-events: all;
|
||||
}
|
||||
.flow-item-status .block-type {
|
||||
pointer-events: none;
|
||||
}
|
||||
.flow-item-status :global(.spectrum-ActionButton),
|
||||
.flow-item-status :global(.spectrum-ActionButton .spectrum-Icon) {
|
||||
color: var(--spectrum-alias-text-color-hover);
|
||||
}
|
||||
.flow-success :global(.spectrum-ActionButton) {
|
||||
background-color: var(--spectrum-semantic-positive-color-status);
|
||||
border-color: var(--spectrum-semantic-positive-color-status);
|
||||
}
|
||||
.flow-error :global(.spectrum-ActionButton) {
|
||||
background-color: var(--spectrum-semantic-negative-color-status);
|
||||
border-color: var(--spectrum-semantic-negative-color-status);
|
||||
}
|
||||
.flow-warn :global(.spectrum-ActionButton) {
|
||||
background-color: var(--spectrum-global-color-gray-300);
|
||||
border-color: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.flow-warn :global(.spectrum-ActionButton .spectrum-Icon) {
|
||||
color: var(--spectrum-global-color-yellow-600);
|
||||
}
|
||||
</style>
|
|
@ -46,16 +46,26 @@
|
|||
...userBindings,
|
||||
...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 } : {}),
|
||||
},
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
|
|
@ -122,7 +122,6 @@
|
|||
await tick()
|
||||
try {
|
||||
await automationStore.actions.test($selectedAutomation.data, testData)
|
||||
$automationStore.showTestPanel = true
|
||||
} catch (error) {
|
||||
notifications.error(error)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,270 @@
|
|||
<script lang="ts">
|
||||
import { ActionButton, Button, Divider, Icon, Modal } from "@budibase/bbui"
|
||||
import {
|
||||
type Automation,
|
||||
type AutomationStep,
|
||||
type AutomationTrigger,
|
||||
type BlockRef,
|
||||
AutomationTriggerStepId,
|
||||
isBranchStep,
|
||||
isTrigger,
|
||||
AutomationFeature,
|
||||
} from "@budibase/types"
|
||||
import { memo } from "@budibase/frontend-core"
|
||||
import {
|
||||
automationStore,
|
||||
selectedAutomation,
|
||||
evaluationContext,
|
||||
} from "@/stores/builder"
|
||||
import BlockData from "../SetupPanel/BlockData.svelte"
|
||||
import BlockProperties from "../SetupPanel/BlockProperties.svelte"
|
||||
import BlockHeader from "../SetupPanel/BlockHeader.svelte"
|
||||
import { PropField } from "../SetupPanel"
|
||||
import RoleSelect from "@/components/common/RoleSelect.svelte"
|
||||
import { type AutomationContext } from "@/stores/builder/automations"
|
||||
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
|
||||
import { getVerticalResizeActions } from "@/components/common/resizable"
|
||||
|
||||
const [resizable, resizableHandle] = getVerticalResizeActions()
|
||||
|
||||
const memoAutomation = memo<Automation | undefined>($selectedAutomation.data)
|
||||
const memoBlock = memo<AutomationStep | AutomationTrigger | undefined>()
|
||||
const memoContext = memo({} as AutomationContext)
|
||||
|
||||
let role: string | undefined
|
||||
let webhookModal: Modal | undefined
|
||||
|
||||
$: memoAutomation.set($selectedAutomation.data)
|
||||
$: memoContext.set($evaluationContext)
|
||||
|
||||
$: selectedNodeId = $automationStore.selectedNodeId
|
||||
$: blockRefs = $selectedAutomation.blockRefs
|
||||
$: blockRef = selectedNodeId ? blockRefs[selectedNodeId] : undefined
|
||||
$: block = automationStore.actions.getBlockByRef($memoAutomation, blockRef)
|
||||
|
||||
$: memoBlock.set(block)
|
||||
|
||||
$: isStep = !!$memoBlock && !isTrigger($memoBlock)
|
||||
$: pathSteps = loadPathSteps(blockRef, $memoAutomation)
|
||||
$: loopBlock = pathSteps.find(
|
||||
x => $memoBlock && x.blockToLoop === $memoBlock.id
|
||||
)
|
||||
|
||||
$: isAppAction = block?.stepId === AutomationTriggerStepId.APP
|
||||
$: isAppAction && fetchPermissions($memoAutomation?._id)
|
||||
$: isAppAction &&
|
||||
automationStore.actions.setPermissions(role, $memoAutomation)
|
||||
|
||||
const fetchPermissions = async (automationId?: string) => {
|
||||
if (!automationId) {
|
||||
return
|
||||
}
|
||||
role = await automationStore.actions.getPermissions(automationId)
|
||||
}
|
||||
|
||||
const loadPathSteps = (
|
||||
blockRef: BlockRef | undefined,
|
||||
automation: Automation | undefined
|
||||
) => {
|
||||
return blockRef && automation
|
||||
? automationStore.actions.getPathSteps(blockRef.pathTo, automation)
|
||||
: []
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={webhookModal}>
|
||||
<CreateWebhookModal />
|
||||
</Modal>
|
||||
|
||||
<div class="panel heading">
|
||||
<div class="details">
|
||||
<BlockHeader
|
||||
automation={$memoAutomation}
|
||||
block={$memoBlock}
|
||||
on:update={e => {
|
||||
if ($memoBlock && !isTrigger($memoBlock)) {
|
||||
automationStore.actions.updateBlockTitle($memoBlock, e.detail)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
on:click={() => {
|
||||
automationStore.actions.selectNode()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if isStep}
|
||||
<div class="step-actions">
|
||||
{#if $memoBlock && !isBranchStep($memoBlock) && ($memoBlock.features?.[AutomationFeature.LOOPING] || !$memoBlock.features)}
|
||||
<ActionButton
|
||||
quiet
|
||||
noPadding
|
||||
icon="RotateCW"
|
||||
on:click={async () => {
|
||||
if (loopBlock) {
|
||||
await automationStore.actions.removeLooping(blockRef)
|
||||
} else {
|
||||
await automationStore.actions.addLooping(blockRef)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loopBlock ? `Stop Looping` : `Loop`}
|
||||
</ActionButton>
|
||||
{/if}
|
||||
<ActionButton
|
||||
quiet
|
||||
noPadding
|
||||
icon="DeleteOutline"
|
||||
on:click={async () => {
|
||||
if (!blockRef) {
|
||||
return
|
||||
}
|
||||
await automationStore.actions.deleteAutomationBlock(blockRef.pathTo)
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</ActionButton>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Divider noMargin />
|
||||
<div class="panel config" use:resizable>
|
||||
<div class="content">
|
||||
{#if loopBlock}
|
||||
<span class="loop">
|
||||
<BlockProperties
|
||||
block={loopBlock}
|
||||
context={$memoContext}
|
||||
automation={$memoAutomation}
|
||||
/>
|
||||
</span>
|
||||
<Divider noMargin />
|
||||
{:else if isAppAction}
|
||||
<PropField label="Role" fullWidth>
|
||||
<RoleSelect bind:value={role} />
|
||||
</PropField>
|
||||
{/if}
|
||||
<span class="props">
|
||||
<BlockProperties
|
||||
block={$memoBlock}
|
||||
context={$memoContext}
|
||||
automation={$memoAutomation}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{#if block?.stepId === AutomationTriggerStepId.WEBHOOK}
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
if (!webhookModal) return
|
||||
webhookModal.show()
|
||||
}}
|
||||
>
|
||||
Set Up Webhook
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
role="separator"
|
||||
class="divider"
|
||||
class:disabled={false}
|
||||
use:resizableHandle
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel data">
|
||||
<BlockData
|
||||
context={$memoContext}
|
||||
block={$memoBlock}
|
||||
automation={$memoAutomation}
|
||||
on:run={() => {
|
||||
automationStore.update(state => ({
|
||||
...state,
|
||||
showTestModal: true,
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.step-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
.heading.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.panel,
|
||||
.config.panel .content {
|
||||
padding: var(--spacing-l);
|
||||
}
|
||||
.config.panel {
|
||||
padding: 0px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 200px;
|
||||
max-height: 550px;
|
||||
transition: height 300ms ease-out, max-height 300ms ease-out;
|
||||
height: 400px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.config.panel .content {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
.config.panel .loop,
|
||||
.config.panel .content .props {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
.data.panel {
|
||||
padding: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.divider {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
transform: translateY(50%);
|
||||
height: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
.divider:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: background 130ms ease-out;
|
||||
}
|
||||
.divider:hover {
|
||||
cursor: row-resize;
|
||||
}
|
||||
.divider:hover:after {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.divider.disabled {
|
||||
cursor: auto;
|
||||
}
|
||||
.divider.disabled:after {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
.details {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,44 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
type AutomationStep,
|
||||
type AutomationTrigger,
|
||||
type EnrichedBinding,
|
||||
} from "@budibase/types"
|
||||
import { PropField } from "."
|
||||
import { type SchemaConfigProps } from "@/types/automations"
|
||||
|
||||
export let block: AutomationStep | AutomationTrigger | undefined = undefined
|
||||
export let context: {} | undefined
|
||||
export let bindings: EnrichedBinding[] | undefined = undefined
|
||||
export let layout: SchemaConfigProps[] | undefined
|
||||
</script>
|
||||
|
||||
{#if layout}
|
||||
{#each layout as config}
|
||||
{#if config.wrapped === false}
|
||||
<svelte:component
|
||||
this={config.comp}
|
||||
{...config?.props ? config.props() : {}}
|
||||
{bindings}
|
||||
{block}
|
||||
{context}
|
||||
on:change={config.onChange}
|
||||
/>
|
||||
{:else}
|
||||
<PropField
|
||||
label={config.title}
|
||||
labelTooltip={config.tooltip || ""}
|
||||
fullWidth
|
||||
>
|
||||
<svelte:component
|
||||
this={config.comp}
|
||||
{...config?.props ? config.props() : {}}
|
||||
{bindings}
|
||||
{block}
|
||||
{context}
|
||||
on:change={config.onChange}
|
||||
/>
|
||||
</PropField>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
|
@ -0,0 +1,389 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
type BaseIOStructure,
|
||||
type AutomationStep,
|
||||
type AutomationTrigger,
|
||||
AutomationActionStepId,
|
||||
AutomationCustomIOType,
|
||||
AutomationIOType,
|
||||
AutomationStepType,
|
||||
} from "@budibase/types"
|
||||
import {
|
||||
AutomationSelector,
|
||||
CronBuilder,
|
||||
DateSelector,
|
||||
ExecuteScript,
|
||||
ExecuteScriptV2,
|
||||
FieldSelector,
|
||||
FileSelector,
|
||||
FilterSelector,
|
||||
PropField,
|
||||
QueryParamSelector,
|
||||
SchemaSetup,
|
||||
TableSelector,
|
||||
} from "."
|
||||
import { getFieldLabel, getInputValue } from "./layouts"
|
||||
import { automationStore, tables } from "@/stores/builder"
|
||||
import {
|
||||
type AutomationSchemaConfig,
|
||||
type FieldProps,
|
||||
type FormUpdate,
|
||||
type FileSelectorMeta,
|
||||
type StepInputs,
|
||||
SchemaFieldTypes,
|
||||
customTypeToSchema,
|
||||
typeToSchema,
|
||||
} from "@/types/automations"
|
||||
import { Select, Checkbox } from "@budibase/bbui"
|
||||
import {
|
||||
DrawerBindableInput,
|
||||
ServerBindingPanel as AutomationBindingPanel,
|
||||
} from "@/components/common/bindings"
|
||||
import Editor from "@/components/integration/QueryEditor.svelte"
|
||||
import WebhookDisplay from "@/components/automation/Shared/WebhookDisplay.svelte"
|
||||
|
||||
export let block: AutomationStep | AutomationTrigger | undefined = undefined
|
||||
export let context: {} | undefined
|
||||
export let bindings: any[] | undefined = undefined
|
||||
|
||||
const SchemaTypes: AutomationSchemaConfig = {
|
||||
[SchemaFieldTypes.ENUM]: {
|
||||
comp: Select,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const field = opts.field
|
||||
return {
|
||||
placeholder: false,
|
||||
options: field.enum,
|
||||
getOptionLabel: (x: string, idx: number) => {
|
||||
return field.pretty ? field.pretty[idx] : x
|
||||
},
|
||||
disabled: field.readonly,
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.TABLE]: {
|
||||
comp: TableSelector,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const field = opts.field
|
||||
return {
|
||||
isTrigger,
|
||||
disabled: field.readonly,
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.FILTER]: {
|
||||
comp: FilterSelector,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { key } = opts
|
||||
return {
|
||||
key,
|
||||
}
|
||||
},
|
||||
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||
const update = e.detail
|
||||
if (block) {
|
||||
automationStore.actions.requestUpdate(update, block)
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.COLUMN]: {
|
||||
comp: Select,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const field = opts.field
|
||||
return {
|
||||
options: Object.keys(table?.schema || {}),
|
||||
disabled: field.readonly,
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.CODE_V2]: {
|
||||
comp: ExecuteScriptV2,
|
||||
fullWidth: true,
|
||||
},
|
||||
[SchemaFieldTypes.BOOL]: {
|
||||
comp: Checkbox,
|
||||
},
|
||||
[SchemaFieldTypes.DATE]: {
|
||||
comp: DateSelector,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { field, key } = opts
|
||||
return {
|
||||
title: field.title ?? getFieldLabel(key, field),
|
||||
disabled: field.readonly,
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.TRIGGER_SCHEMA]: {
|
||||
comp: SchemaSetup,
|
||||
fullWidth: true,
|
||||
},
|
||||
[SchemaFieldTypes.JSON]: {
|
||||
comp: Editor,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { field, value: editorBody } = opts
|
||||
return {
|
||||
value: editorBody?.value,
|
||||
editorHeight: "250",
|
||||
editorWidth: "448",
|
||||
mode: "json",
|
||||
readOnly: field.readonly,
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.FILE]: {
|
||||
comp: FileSelector,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { field } = opts
|
||||
const meta = getInputValue(inputData, "meta") as FileSelectorMeta
|
||||
return {
|
||||
buttonText:
|
||||
field.type === "attachment" ? "Add attachment" : "Add signature",
|
||||
useAttachmentBinding: meta?.useAttachmentBinding,
|
||||
}
|
||||
},
|
||||
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||
const update = e.detail
|
||||
if (block) {
|
||||
automationStore.actions.requestUpdate(update, block)
|
||||
}
|
||||
},
|
||||
fullWidth: true,
|
||||
},
|
||||
[SchemaFieldTypes.CRON]: {
|
||||
comp: CronBuilder,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { value } = opts
|
||||
return {
|
||||
cronExpression: value,
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.LOOP_OPTION]: {
|
||||
comp: Select,
|
||||
props: () => {
|
||||
return {
|
||||
autoWidth: true,
|
||||
options: ["Array", "String"],
|
||||
defaultValue: "Array",
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.AUTOMATION_FIELDS]: {
|
||||
comp: AutomationSelector,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { field } = opts
|
||||
return {
|
||||
title: field.title,
|
||||
}
|
||||
},
|
||||
fullWidth: true,
|
||||
wrapped: false,
|
||||
},
|
||||
[SchemaFieldTypes.WEBHOOK_URL]: {
|
||||
comp: WebhookDisplay,
|
||||
},
|
||||
[SchemaFieldTypes.QUERY_PARAMS]: {
|
||||
comp: QueryParamSelector,
|
||||
},
|
||||
[SchemaFieldTypes.CODE]: {
|
||||
comp: ExecuteScript,
|
||||
fullWidth: true,
|
||||
},
|
||||
[SchemaFieldTypes.STRING]: {
|
||||
comp: DrawerBindableInput,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { key, field } = opts
|
||||
return {
|
||||
title: field.title ?? getFieldLabel(key, field),
|
||||
panel: AutomationBindingPanel,
|
||||
type: field.customType,
|
||||
updateOnChange: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.QUERY_LIMIT]: {
|
||||
comp: DrawerBindableInput,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { key, field } = opts
|
||||
return {
|
||||
title: field.title ?? getFieldLabel(key, field),
|
||||
panel: AutomationBindingPanel,
|
||||
type: field.customType,
|
||||
updateOnChange: false,
|
||||
placeholder: queryLimit,
|
||||
}
|
||||
},
|
||||
},
|
||||
// Not a core schema type.
|
||||
[SchemaFieldTypes.FIELDS]: {
|
||||
comp: FieldSelector,
|
||||
props: () => {
|
||||
return {
|
||||
isTestModal: false,
|
||||
}
|
||||
},
|
||||
fullWidth: true,
|
||||
},
|
||||
}
|
||||
|
||||
$: isTrigger = block?.type === AutomationStepType.TRIGGER
|
||||
|
||||
// The step input properties
|
||||
$: inputData = automationStore.actions.getInputData(block)
|
||||
|
||||
// Automation definition properties
|
||||
$: schemaProperties = Object.entries(block?.schema?.inputs?.properties || {})
|
||||
|
||||
// Used for field labelling
|
||||
$: requiredProperties = block ? block.schema["inputs"].required : []
|
||||
|
||||
$: tableId = inputData && "tableId" in inputData ? inputData.tableId : null
|
||||
$: table = tableId
|
||||
? $tables.list.find(table => table._id === tableId)
|
||||
: { schema: {} }
|
||||
|
||||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||
|
||||
const canShowField = (value: BaseIOStructure, inputData?: StepInputs) => {
|
||||
if (!inputData) {
|
||||
console.error("Cannot determine field visibility without field data")
|
||||
return false
|
||||
}
|
||||
const dependsOn = value?.dependsOn
|
||||
return !dependsOn || !!getInputValue(inputData, dependsOn)
|
||||
}
|
||||
|
||||
const defaultChange = (
|
||||
update: FormUpdate,
|
||||
block?: AutomationTrigger | AutomationStep
|
||||
) => {
|
||||
if (block) {
|
||||
automationStore.actions.requestUpdate(update, block)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Build the core props used to page the Automation Schema field.
|
||||
*
|
||||
* @param key
|
||||
* @param field
|
||||
* @param block
|
||||
*/
|
||||
const schemaToUI = (
|
||||
key: string,
|
||||
field: BaseIOStructure,
|
||||
block?: AutomationStep | AutomationTrigger
|
||||
) => {
|
||||
if (!block) {
|
||||
console.error("Could not process UI elements.")
|
||||
return {}
|
||||
}
|
||||
const type = getFieldType(field, block)
|
||||
const config = type ? SchemaTypes[type] : null
|
||||
const title = getFieldLabel(key, field, requiredProperties?.includes(key))
|
||||
const value = getInputValue(inputData, key)
|
||||
const meta = getInputValue(inputData, "meta")
|
||||
|
||||
return {
|
||||
meta,
|
||||
config,
|
||||
value,
|
||||
title,
|
||||
props: config?.props
|
||||
? config.props({ key, field, block, value, meta, title })
|
||||
: {},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the correct UI element based on the automation
|
||||
* schema configuration.
|
||||
*
|
||||
* @param field
|
||||
* @param block
|
||||
*/
|
||||
const getFieldType = (
|
||||
field: BaseIOStructure,
|
||||
block?: AutomationStep | AutomationTrigger
|
||||
) => {
|
||||
// Direct customType map
|
||||
const customType = field.customType && customTypeToSchema[field.customType]
|
||||
if (customType) {
|
||||
return customType
|
||||
}
|
||||
|
||||
// Direct type map
|
||||
const fieldType = field.type && typeToSchema[field.type]
|
||||
if (fieldType) {
|
||||
return fieldType
|
||||
}
|
||||
|
||||
// Enum Field
|
||||
if (field.type === AutomationIOType.STRING && field.enum) {
|
||||
return SchemaFieldTypes.ENUM
|
||||
}
|
||||
|
||||
// JS V2
|
||||
if (
|
||||
field.customType === AutomationCustomIOType.CODE &&
|
||||
block?.stepId === AutomationActionStepId.EXECUTE_SCRIPT_V2
|
||||
) {
|
||||
return SchemaFieldTypes.CODE_V2
|
||||
}
|
||||
|
||||
// Filter step or trigger
|
||||
if (
|
||||
field.customType === AutomationCustomIOType.FILTERS ||
|
||||
field.customType === AutomationCustomIOType.TRIGGER_FILTER
|
||||
) {
|
||||
return SchemaFieldTypes.FILTER
|
||||
}
|
||||
|
||||
// Default string/number UX
|
||||
if (
|
||||
field.type === AutomationIOType.STRING ||
|
||||
field.type === AutomationIOType.NUMBER
|
||||
) {
|
||||
// "integer" removed as it isnt present in the type
|
||||
return SchemaFieldTypes.STRING
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each schemaProperties as [key, field]}
|
||||
{#if canShowField(field, inputData)}
|
||||
{@const { config, props, value, title } = schemaToUI(key, field, block)}
|
||||
{#if config}
|
||||
{#if config.wrapped === false}
|
||||
<svelte:component
|
||||
this={config.comp}
|
||||
{bindings}
|
||||
{block}
|
||||
{context}
|
||||
{key}
|
||||
{...{ value, ...props }}
|
||||
on:change={config.onChange ??
|
||||
(e => {
|
||||
defaultChange({ [key]: e.detail }, block)
|
||||
})}
|
||||
/>
|
||||
{:else}
|
||||
<PropField label={title} fullWidth labelTooltip={config.tooltip || ""}>
|
||||
<svelte:component
|
||||
this={config.comp}
|
||||
{bindings}
|
||||
{block}
|
||||
{context}
|
||||
{key}
|
||||
{...{ value, ...props }}
|
||||
on:change={config.onChange ??
|
||||
(e => {
|
||||
defaultChange({ [key]: e.detail }, block)
|
||||
})}
|
||||
/>
|
||||
</PropField>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
|
@ -1,15 +1,18 @@
|
|||
<script>
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,385 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
Divider,
|
||||
ActionButton,
|
||||
notifications,
|
||||
Helpers,
|
||||
Icon,
|
||||
Button,
|
||||
} from "@budibase/bbui"
|
||||
import JSONViewer, {
|
||||
type JSONViewerClickEvent,
|
||||
} from "@/components/common/JSONViewer.svelte"
|
||||
import {
|
||||
type AutomationStep,
|
||||
type AutomationTrigger,
|
||||
type Automation,
|
||||
type LoopStepInputs,
|
||||
type DidNotTriggerResponse,
|
||||
type TestAutomationResponse,
|
||||
type BlockRef,
|
||||
type AutomationIOProps,
|
||||
type AutomationStepResult,
|
||||
AutomationStepStatus,
|
||||
AutomationStatus,
|
||||
AutomationStepType,
|
||||
isDidNotTriggerResponse,
|
||||
isTrigger,
|
||||
isFilterStep,
|
||||
isLoopStep,
|
||||
BlockDefinitionTypes,
|
||||
AutomationActionStepId,
|
||||
} from "@budibase/types"
|
||||
import Count from "./Count.svelte"
|
||||
import { automationStore, selectedAutomation } from "@/stores/builder"
|
||||
import {
|
||||
type BlockStatus,
|
||||
BlockStatusSource,
|
||||
BlockStatusType,
|
||||
DataMode,
|
||||
RowSteps,
|
||||
} from "@/types/automations"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { cloneDeep } from "lodash"
|
||||
import { type AutomationContext } from "@/stores/builder/automations"
|
||||
|
||||
export let context: AutomationContext | undefined
|
||||
export let block: AutomationStep | AutomationTrigger | undefined
|
||||
export let automation: Automation | undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const DataModeTabs = {
|
||||
[DataMode.INPUT]: "Data In",
|
||||
[DataMode.OUTPUT]: "Data Out",
|
||||
[DataMode.ERRORS]: "Errors",
|
||||
}
|
||||
|
||||
// Add explicit weight to the issue types
|
||||
const issueOrder = {
|
||||
[BlockStatusType.ERROR]: 0,
|
||||
[BlockStatusType.WARN]: 1,
|
||||
[BlockStatusType.INFO]: 2,
|
||||
}
|
||||
|
||||
let dataMode: DataMode = DataMode.INPUT
|
||||
let issues: BlockStatus[] = []
|
||||
|
||||
$: blockRef = block?.id
|
||||
? $selectedAutomation?.blockRefs?.[block.id]
|
||||
: undefined
|
||||
|
||||
// The loop block associated with the selected block
|
||||
$: loopRef = blockRef?.looped
|
||||
? $selectedAutomation.blockRefs[blockRef.looped]
|
||||
: undefined
|
||||
$: loopBlock = automationStore.actions.getBlockByRef(automation, loopRef)
|
||||
|
||||
$: if ($automationStore.selectedNodeId) {
|
||||
dataMode = $automationStore.selectedNodeMode || DataMode.INPUT
|
||||
}
|
||||
|
||||
$: testResults = $automationStore.testResults as TestAutomationResponse
|
||||
$: blockResults = automationStore.actions.processBlockResults(
|
||||
testResults,
|
||||
block
|
||||
)
|
||||
$: processTestIssues(testResults, block)
|
||||
|
||||
/**
|
||||
* Take the results of an automation and generate a
|
||||
* list of graded issues. If the conditions get bigger they
|
||||
* could be broken out.
|
||||
*
|
||||
* @param testResults
|
||||
* @param block
|
||||
*/
|
||||
const processTestIssues = (
|
||||
testResults: TestAutomationResponse,
|
||||
block: AutomationStep | AutomationTrigger | undefined
|
||||
) => {
|
||||
// Reset issues
|
||||
issues = []
|
||||
|
||||
if (!testResults || !block || !blockResults) {
|
||||
return
|
||||
}
|
||||
|
||||
// Process loop issues
|
||||
if (blockRef?.looped && loopBlock && isLoopStep(loopBlock)) {
|
||||
const inputs = loopBlock.inputs as LoopStepInputs
|
||||
const loopMax = Number(inputs.iterations)
|
||||
const loopOutputs = blockResults?.outputs
|
||||
|
||||
let loopMessage = "There was an error"
|
||||
|
||||
// Not technically failed as it continues to run
|
||||
if (loopOutputs?.status === AutomationStepStatus.MAX_ITERATIONS) {
|
||||
loopMessage = `The maximum number of iterations (${loopMax}) has been reached.`
|
||||
} else if (
|
||||
loopOutputs?.status === AutomationStepStatus.FAILURE_CONDITION
|
||||
) {
|
||||
loopMessage = `The failure condition for the loop was hit: ${inputs.failure}.
|
||||
The loop was terminated`
|
||||
} else if (loopOutputs?.status === AutomationStepStatus.INCORRECT_TYPE) {
|
||||
loopMessage = `An 'Input Type' of '${inputs.option}' was configured which does
|
||||
not match the value supplied`
|
||||
}
|
||||
|
||||
issues.push({
|
||||
message: loopMessage,
|
||||
type: BlockStatusType.ERROR,
|
||||
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||
})
|
||||
}
|
||||
|
||||
// Process filtered row issues
|
||||
else if (isTrigger(block) && isDidNotTriggerResponse(testResults)) {
|
||||
issues.push({
|
||||
message: (blockResults as DidNotTriggerResponse).message,
|
||||
type: BlockStatusType.WARN,
|
||||
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||
})
|
||||
}
|
||||
|
||||
// Filter step
|
||||
else if (
|
||||
isFilterStep(block) &&
|
||||
blockResults?.outputs.status === AutomationStatus.STOPPED
|
||||
) {
|
||||
issues.push({
|
||||
message: `The conditions were not met and
|
||||
the automation flow was stopped.`,
|
||||
type: BlockStatusType.WARN,
|
||||
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||
})
|
||||
}
|
||||
|
||||
// Row step issues
|
||||
else if (!isTrigger(block) && RowSteps.includes(block.stepId)) {
|
||||
const outputs = (blockResults as AutomationStepResult)?.outputs
|
||||
if (outputs.success) return
|
||||
issues.push({
|
||||
message: `Could not complete the row request:
|
||||
${outputs.response?.message || JSON.stringify(outputs.response)}`,
|
||||
type: BlockStatusType.ERROR,
|
||||
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||
})
|
||||
}
|
||||
|
||||
// Default on error.
|
||||
else if (!isTrigger(block) && !blockResults.outputs.success) {
|
||||
issues.push({
|
||||
message: `There was an issue with the step. See 'Data out' for more information`,
|
||||
type: BlockStatusType.ERROR,
|
||||
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort the issues
|
||||
issues.sort((a, b) => issueOrder[a.type] - issueOrder[b.type])
|
||||
}
|
||||
|
||||
const copyContext = (e: JSONViewerClickEvent) => {
|
||||
Helpers.copyToClipboard(JSON.stringify(e.detail?.value))
|
||||
notifications.success("Copied to clipboard")
|
||||
}
|
||||
|
||||
/**
|
||||
* Take the core AutomationContext and humanise it
|
||||
* Before the test is run, populate the "Data in" values
|
||||
* with the schema definition keyed by the readable step names.
|
||||
*/
|
||||
const parseContext = (context?: AutomationContext, blockRef?: BlockRef) => {
|
||||
if (!blockRef || !automation) {
|
||||
return
|
||||
}
|
||||
|
||||
let clonetext = cloneDeep(context)
|
||||
|
||||
const pathSteps = automationStore.actions.getPathSteps(
|
||||
blockRef.pathTo,
|
||||
automation
|
||||
)
|
||||
|
||||
const defs = $automationStore.blockDefinitions
|
||||
const stepNames = automation.definition.stepNames
|
||||
const loopDef =
|
||||
defs[BlockDefinitionTypes.ACTION]?.[AutomationActionStepId.LOOP]
|
||||
|
||||
// Exclude the trigger and loops from steps
|
||||
const filteredSteps: Record<string, AutomationStep | AutomationIOProps> =
|
||||
pathSteps
|
||||
.slice(0, -1)
|
||||
.filter(
|
||||
(step): step is AutomationStep =>
|
||||
step.type === AutomationStepType.ACTION &&
|
||||
step.stepId !== AutomationActionStepId.LOOP
|
||||
)
|
||||
.reduce(
|
||||
(
|
||||
acc: Record<string, AutomationStep | AutomationIOProps>,
|
||||
step: AutomationStep
|
||||
) => {
|
||||
// Process the block
|
||||
const blockRef = $selectedAutomation.blockRefs[step.id]
|
||||
const blockDefinition =
|
||||
defs[BlockDefinitionTypes.ACTION]?.[step.stepId]
|
||||
|
||||
// Check if the block has a loop
|
||||
const loopRef = blockRef.looped
|
||||
? $selectedAutomation.blockRefs[blockRef.looped]
|
||||
: undefined
|
||||
|
||||
const testData = context?.steps?.[step.id]
|
||||
const sampleDef = loopRef ? loopDef : blockDefinition
|
||||
|
||||
// Prioritise the display of testData and fallback to step defs
|
||||
// This will ensure users have at least an idea of what they are looking at
|
||||
acc[stepNames?.[step.id] || step.name] =
|
||||
testData || sampleDef?.schema.outputs.properties || {}
|
||||
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, AutomationStep | AutomationIOProps>
|
||||
)
|
||||
|
||||
return { ...clonetext, steps: filteredSteps }
|
||||
}
|
||||
|
||||
$: parsedContext = parseContext(context, blockRef)
|
||||
</script>
|
||||
|
||||
<div class="tabs">
|
||||
{#each Object.values(DataMode) as mode}
|
||||
<Count count={mode === DataMode.ERRORS ? issues.length : 0}>
|
||||
<ActionButton
|
||||
selected={mode === dataMode}
|
||||
quiet
|
||||
on:click={() => {
|
||||
dataMode = mode
|
||||
}}
|
||||
>
|
||||
{DataModeTabs[mode]}
|
||||
</ActionButton>
|
||||
</Count>
|
||||
{/each}
|
||||
</div>
|
||||
<Divider noMargin />
|
||||
<div class="viewer">
|
||||
{#if dataMode === DataMode.INPUT}
|
||||
<JSONViewer
|
||||
value={parsedContext}
|
||||
showCopyIcon
|
||||
on:click-copy={copyContext}
|
||||
/>
|
||||
{:else if dataMode === DataMode.OUTPUT}
|
||||
{#if blockResults}
|
||||
<JSONViewer
|
||||
value={blockResults.outputs}
|
||||
showCopyIcon
|
||||
on:click-copy={copyContext}
|
||||
/>
|
||||
{:else if testResults && !blockResults}
|
||||
<div class="content">
|
||||
<span class="info">
|
||||
This step was not executed as part of the test run
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="content">
|
||||
<span class="info">
|
||||
Run the automation to show the output of this step
|
||||
</span>
|
||||
<Button
|
||||
size={"S"}
|
||||
icon={"Play"}
|
||||
secondary
|
||||
on:click={() => {
|
||||
dispatch("run")
|
||||
}}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="issues" class:empty={!issues.length}>
|
||||
{#if issues.length === 0}
|
||||
<span>There are no current issues</span>
|
||||
{:else}
|
||||
{#each issues as issue}
|
||||
<div class={`issue ${issue.type}`}>
|
||||
<div class="icon"><Icon name="Alert" /></div>
|
||||
<!-- For custom automations, the error message needs a default -->
|
||||
<div class="message">
|
||||
{issue.message || "There was an error"}
|
||||
</div>
|
||||
</div>
|
||||
<Divider noMargin />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.content .info {
|
||||
text-align: center;
|
||||
max-width: 70%;
|
||||
}
|
||||
.tabs,
|
||||
.viewer {
|
||||
padding: var(--spacing-l);
|
||||
}
|
||||
.viewer {
|
||||
overflow-y: scroll;
|
||||
flex: 1;
|
||||
padding-right: 0px;
|
||||
}
|
||||
.viewer .content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
padding: var(--spacing-m) var(--spacing-l);
|
||||
}
|
||||
.issue {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: var(--spacing-l);
|
||||
}
|
||||
.issues {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.issues.empty {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.issue.error .icon {
|
||||
color: var(--spectrum-global-color-static-red-600);
|
||||
}
|
||||
.issue.warn .icon {
|
||||
color: var(--spectrum-global-color-static-yellow-600);
|
||||
}
|
||||
.issues :global(hr.spectrum-Divider:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.issues .issue:not(:first-child) {
|
||||
padding-top: var(--spacing-l);
|
||||
}
|
||||
.issues {
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,236 @@
|
|||
<script lang="ts">
|
||||
import { Body, Tags, Tag, Icon, TooltipPosition } from "@budibase/bbui"
|
||||
import {
|
||||
type AutomationStep,
|
||||
type AutomationTrigger,
|
||||
type Automation,
|
||||
AutomationStepType,
|
||||
isBranchStep,
|
||||
isActionStep,
|
||||
} from "@budibase/types"
|
||||
import {
|
||||
type ExternalAction,
|
||||
externalActions,
|
||||
} from "@/components/automation/AutomationBuilder/FlowChart/ExternalActions"
|
||||
import { automationStore } from "@/stores/builder"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let block: AutomationStep | AutomationTrigger | undefined = undefined
|
||||
export let automation: Automation | undefined = undefined
|
||||
export let itemName: string | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let externalAction: ExternalAction | undefined
|
||||
let editing: boolean
|
||||
let validRegex: RegExp = /^[A-Za-z0-9_\s]+$/
|
||||
|
||||
$: stepNames = automation?.definition.stepNames || {}
|
||||
$: blockHeading = getHeading(itemName, block) || ""
|
||||
$: blockNameError = getStepNameError(blockHeading)
|
||||
$: isBranch = block && isBranchStep(block)
|
||||
|
||||
const getHeading = (
|
||||
itemName?: string,
|
||||
block?: AutomationStep | AutomationTrigger
|
||||
) => {
|
||||
if (itemName) {
|
||||
return itemName
|
||||
} else if (block) {
|
||||
return stepNames?.[block?.id] || block?.name
|
||||
}
|
||||
}
|
||||
|
||||
$: isTrigger = block?.type === AutomationStepType.TRIGGER
|
||||
$: allSteps = automation?.definition.steps || []
|
||||
$: blockDefinition = automationStore.actions.getBlockDefinition(block)
|
||||
|
||||
// Put the type in the header if they change the name
|
||||
// Otherwise the info is obscured
|
||||
$: blockTitle = isBranch
|
||||
? "Branch"
|
||||
: `${
|
||||
block &&
|
||||
block?.id in stepNames &&
|
||||
stepNames[block?.id] !== blockDefinition?.name
|
||||
? blockDefinition?.name
|
||||
: "Step"
|
||||
}`
|
||||
|
||||
// Parse external actions
|
||||
$: if (block && isActionStep(block) && block?.stepId in externalActions) {
|
||||
externalAction = externalActions[block?.stepId]
|
||||
} else {
|
||||
externalAction = undefined
|
||||
}
|
||||
|
||||
const getStepNameError = (name: string) => {
|
||||
if (!block) {
|
||||
return
|
||||
}
|
||||
const duplicateError =
|
||||
"This name already exists, please enter a unique name"
|
||||
if (editing) {
|
||||
for (const [key, value] of Object.entries(stepNames)) {
|
||||
if (name !== block.name && name === value && key !== block.id) {
|
||||
return duplicateError
|
||||
}
|
||||
}
|
||||
|
||||
for (const step of allSteps) {
|
||||
if (step.id !== block.id && name === step.name) {
|
||||
return duplicateError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (name !== block.name && name?.length > 0) {
|
||||
let invalidRoleName = !validRegex.test(name)
|
||||
if (invalidRoleName) {
|
||||
return "Please enter a name consisting of only alphanumeric symbols and underscores"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const startEditing = () => {
|
||||
editing = true
|
||||
}
|
||||
|
||||
const stopEditing = () => {
|
||||
editing = false
|
||||
if (blockNameError) {
|
||||
blockHeading =
|
||||
(block ? stepNames[block?.id] : undefined) || block?.name || ""
|
||||
} else {
|
||||
dispatch("update", blockHeading)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
blockHeading = target.value.trim()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="block-details">
|
||||
{#if block}
|
||||
{#if externalAction}
|
||||
<img
|
||||
alt={externalAction.name}
|
||||
src={externalAction.icon}
|
||||
width="28px"
|
||||
height="28px"
|
||||
/>
|
||||
{:else}
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
class="spectrum-Icon"
|
||||
style="color:var(--spectrum-global-color-gray-700);"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-{block.icon}" />
|
||||
</svg>
|
||||
{/if}
|
||||
<div class="heading">
|
||||
{#if isTrigger}
|
||||
<Body size="XS"><b>Trigger</b></Body>
|
||||
{:else}
|
||||
<Body size="XS">
|
||||
<div class="step">
|
||||
<b>{blockTitle}</b>
|
||||
{#if blockDefinition?.deprecated}
|
||||
<Tags>
|
||||
<Tag invalid>Deprecated</Tag>
|
||||
</Tags>
|
||||
{/if}
|
||||
</div>
|
||||
</Body>
|
||||
{/if}
|
||||
<input
|
||||
class="input-text"
|
||||
placeholder={`Enter step name`}
|
||||
name="name"
|
||||
autocomplete="off"
|
||||
disabled={isTrigger || disabled}
|
||||
value={blockHeading}
|
||||
on:input={handleInput}
|
||||
on:click={e => {
|
||||
e.stopPropagation()
|
||||
startEditing()
|
||||
}}
|
||||
on:keydown={async e => {
|
||||
if (e.key === "Enter") {
|
||||
stopEditing()
|
||||
}
|
||||
}}
|
||||
on:blur={stopEditing}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if blockNameError && editing}
|
||||
<div class="error-container">
|
||||
<div class="error-icon">
|
||||
<Icon
|
||||
size="S"
|
||||
name="Alert"
|
||||
tooltip={blockNameError}
|
||||
tooltipPosition={TooltipPosition.Left}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
input {
|
||||
color: var(--ink);
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-text {
|
||||
font-size: var(--spectrum-alias-font-size-default);
|
||||
font-family: var(--font-sans);
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 0px;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Hide arrows for number fields */
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.block-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-l);
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.error-icon :global(.spectrum-Icon) {
|
||||
fill: var(--spectrum-global-color-red-600);
|
||||
}
|
||||
|
||||
.heading {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,65 @@
|
|||
<script lang="ts">
|
||||
import { automationStore } from "@/stores/builder"
|
||||
import { memo } from "@budibase/frontend-core"
|
||||
import { environment } from "@/stores/portal"
|
||||
import {
|
||||
type AutomationStep,
|
||||
type AutomationTrigger,
|
||||
type Automation,
|
||||
} from "@budibase/types"
|
||||
import { type SchemaConfigProps } from "@/types/automations"
|
||||
import { writable } from "svelte/store"
|
||||
import { getCustomStepLayout } from "./layouts"
|
||||
import AutomationSchemaLayout from "./AutomationSchemaLayout.svelte"
|
||||
import AutomationCustomLayout from "./AutomationCustomLayout.svelte"
|
||||
|
||||
export let block: AutomationStep | AutomationTrigger | undefined = undefined
|
||||
export let automation: Automation | undefined = undefined
|
||||
export let context: {} | undefined
|
||||
|
||||
const memoEnvVariables = memo($environment.variables)
|
||||
|
||||
// Databindings requires TypeScript conversion
|
||||
let environmentBindings: any[]
|
||||
|
||||
// All bindings available to this point
|
||||
$: availableBindings = automationStore.actions.getPathBindings(
|
||||
block?.id,
|
||||
automation
|
||||
)
|
||||
|
||||
// Fetch the env bindings
|
||||
$: if ($memoEnvVariables) {
|
||||
environmentBindings = automationStore.actions.buildEnvironmentBindings()
|
||||
}
|
||||
$: userBindings = automationStore.actions.buildUserBindings()
|
||||
$: settingBindings = automationStore.actions.buildSettingBindings()
|
||||
|
||||
// Combine all bindings for the step
|
||||
$: bindings = [
|
||||
...availableBindings,
|
||||
...environmentBindings,
|
||||
...userBindings,
|
||||
...settingBindings,
|
||||
]
|
||||
|
||||
// Store for any UX related data
|
||||
const stepStore = writable<Record<string, any>>({})
|
||||
$: if (block?.id) {
|
||||
stepStore.update(state => ({ ...state, [block.id]: {} }))
|
||||
}
|
||||
|
||||
// Determine if any custom step layouts have been created
|
||||
let customLayout: SchemaConfigProps[] | undefined
|
||||
$: if ($stepStore) {
|
||||
customLayout = getCustomStepLayout(block, stepStore)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if customLayout}
|
||||
<!-- Render custom layout 1 or more components in a custom layout -->
|
||||
<AutomationCustomLayout {context} {bindings} {block} layout={customLayout} />
|
||||
{:else}
|
||||
<!-- Render Automation Step Schema > [string, BaseIOStructure][] -->
|
||||
<AutomationSchemaLayout {context} {bindings} {block} />
|
||||
{/if}
|
|
@ -0,0 +1,51 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { AbsTooltip } from "@budibase/bbui"
|
||||
|
||||
export let count: number = 0
|
||||
export let tooltip: string | undefined = undefined
|
||||
export let hoverable: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<AbsTooltip text={tooltip}>
|
||||
{#if count}
|
||||
<span
|
||||
class="count"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
aria-label={`Notifications ${count}`}
|
||||
class:hoverable
|
||||
on:mouseenter={() => {
|
||||
dispatch("hover")
|
||||
}}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
{/if}
|
||||
</AbsTooltip>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.count {
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
top: -6px;
|
||||
background: var(--spectrum-global-color-static-red-600);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
padding: 0 4px;
|
||||
z-index: 2;
|
||||
font-size: 0.8em;
|
||||
cursor: default;
|
||||
}
|
||||
.count.hoverable {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,37 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
DrawerBindableSlot,
|
||||
ServerBindingPanel as AutomationBindingPanel,
|
||||
} from "@/components/common/bindings"
|
||||
import { DatePicker } from "@budibase/bbui"
|
||||
import { type EnrichedBinding } from "@budibase/types"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let context: Record<any, any> | undefined = undefined
|
||||
export let bindings: EnrichedBinding[] | undefined = undefined
|
||||
export let value: string | undefined = undefined
|
||||
export let title: string | undefined = undefined
|
||||
export let disabled: boolean | undefined = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<DrawerBindableSlot
|
||||
{title}
|
||||
panel={AutomationBindingPanel}
|
||||
type={"date"}
|
||||
{value}
|
||||
on:change={e => dispatch("change", e.detail)}
|
||||
{bindings}
|
||||
allowJS={true}
|
||||
updateOnChange={false}
|
||||
{disabled}
|
||||
{context}
|
||||
>
|
||||
<DatePicker
|
||||
{value}
|
||||
on:change={e => {
|
||||
dispatch("change", e.detail)
|
||||
}}
|
||||
/>
|
||||
</DrawerBindableSlot>
|
|
@ -0,0 +1,111 @@
|
|||
<script lang="ts">
|
||||
import { BindingSidePanel } from "@/components/common/bindings"
|
||||
import {
|
||||
BindingType,
|
||||
BindingHelpers,
|
||||
} from "@/components/common/bindings/utils"
|
||||
import CodeEditor from "@/components/common/CodeEditor/CodeEditor.svelte"
|
||||
import {
|
||||
bindingsToCompletions,
|
||||
hbAutocomplete,
|
||||
EditorModes,
|
||||
} from "@/components/common/CodeEditor"
|
||||
import { CodeEditorModal } from "@/components/automation/SetupPanel"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import {
|
||||
AutomationActionStepId,
|
||||
type AutomationStep,
|
||||
type CaretPositionFn,
|
||||
type InsertAtPositionFn,
|
||||
type EnrichedBinding,
|
||||
BindingMode,
|
||||
} from "@budibase/types"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value: string | undefined
|
||||
export let isJS: boolean = true
|
||||
export let block: AutomationStep
|
||||
export let context
|
||||
export let bindings
|
||||
|
||||
let getCaretPosition: CaretPositionFn | undefined
|
||||
let insertAtPos: InsertAtPositionFn | undefined
|
||||
|
||||
$: codeMode =
|
||||
block.stepId === AutomationActionStepId.EXECUTE_BASH
|
||||
? EditorModes.Handlebars
|
||||
: EditorModes.JS
|
||||
$: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, {
|
||||
disableWrapping: true,
|
||||
})
|
||||
$: editingJs = codeMode === EditorModes.JS
|
||||
$: stepCompletions =
|
||||
codeMode === EditorModes.Handlebars
|
||||
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||
: []
|
||||
|
||||
const addBinding = (binding: EnrichedBinding) =>
|
||||
bindingsHelpers.onSelectBinding(value, binding, {
|
||||
js: true,
|
||||
dontDecode: true,
|
||||
type: BindingType.RUNTIME,
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- DEPRECATED -->
|
||||
<CodeEditorModal
|
||||
on:hide={() => {
|
||||
// Push any pending changes when the window closes
|
||||
dispatch("change", value)
|
||||
}}
|
||||
>
|
||||
<div class:js-editor={isJS}>
|
||||
<div class:js-code={isJS} style="width:100%;height:500px;">
|
||||
<CodeEditor
|
||||
{value}
|
||||
on:change={e => {
|
||||
// need to pass without the value inside
|
||||
value = e.detail
|
||||
}}
|
||||
completions={stepCompletions}
|
||||
mode={codeMode}
|
||||
autocompleteEnabled={codeMode !== EditorModes.JS}
|
||||
bind:getCaretPosition
|
||||
bind:insertAtPos
|
||||
placeholder={codeMode === EditorModes.Handlebars
|
||||
? "Add bindings by typing {{"
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
{#if editingJs}
|
||||
<div class="js-binding-picker">
|
||||
<BindingSidePanel
|
||||
{bindings}
|
||||
allowHelpers={false}
|
||||
{addBinding}
|
||||
mode={BindingMode.JavaScript}
|
||||
{context}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</CodeEditorModal>
|
||||
|
||||
<style>
|
||||
.js-editor {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.js-code {
|
||||
flex: 7;
|
||||
}
|
||||
|
||||
.js-binding-picker {
|
||||
flex: 3;
|
||||
margin-top: calc((var(--spacing-xl) * -1) + 1px);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,67 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
DrawerBindableSlot,
|
||||
ServerBindingPanel as AutomationBindingPanel,
|
||||
} from "@/components/common/bindings"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { type EnrichedBinding, FieldType } from "@budibase/types"
|
||||
import CodeEditorField from "@/components/common/bindings/CodeEditorField.svelte"
|
||||
import { DropdownPosition } from "@/components/common/CodeEditor/CodeEditor.svelte"
|
||||
|
||||
export let value: string
|
||||
export let context: Record<any, any> | undefined = undefined
|
||||
export let bindings: EnrichedBinding[] | undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="scriptv2-wrapper">
|
||||
<DrawerBindableSlot
|
||||
title={"Edit Code"}
|
||||
panel={AutomationBindingPanel}
|
||||
type={FieldType.LONGFORM}
|
||||
on:change={e => dispatch("change", e.detail)}
|
||||
{value}
|
||||
{bindings}
|
||||
allowJS={true}
|
||||
allowHBS={false}
|
||||
updateOnChange={false}
|
||||
{context}
|
||||
>
|
||||
<div class="field-wrap code-editor">
|
||||
<CodeEditorField
|
||||
{value}
|
||||
{bindings}
|
||||
{context}
|
||||
placeholder={"Add bindings by typing $"}
|
||||
on:change={e => dispatch("change", e.detail)}
|
||||
dropdown={DropdownPosition.Relative}
|
||||
/>
|
||||
</div>
|
||||
</DrawerBindableSlot>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.field-wrap :global(.cm-editor),
|
||||
.field-wrap :global(.cm-scroller) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.field-wrap {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--spectrum-global-color-gray-400);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.field-wrap.code-editor {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.scriptv2-wrapper :global(.icon.slot-icon),
|
||||
.scriptv2-wrapper :global(.text-area-slot-icon) {
|
||||
right: 1px;
|
||||
top: 1px;
|
||||
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
|
||||
border-bottom-left-radius: var(--spectrum-alias-border-radius-regular);
|
||||
border-right: 0px;
|
||||
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,89 @@
|
|||
<script lang="ts">
|
||||
import KeyValueBuilder from "@/components/integration/KeyValueBuilder.svelte"
|
||||
import { Toggle } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import {
|
||||
DrawerBindableInput,
|
||||
ServerBindingPanel as AutomationBindingPanel,
|
||||
} from "@/components/common/bindings"
|
||||
import { type KeyValuePair } from "@/types/automations"
|
||||
|
||||
export let useAttachmentBinding
|
||||
export let buttonText
|
||||
export let key
|
||||
export let context
|
||||
export let value
|
||||
export let bindings
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
interface Attachment {
|
||||
filename: string
|
||||
url: string
|
||||
}
|
||||
|
||||
const handleAttachmentParams = (keyValueObj: Attachment[]) => {
|
||||
let params: Record<string, string> = {}
|
||||
if (keyValueObj?.length) {
|
||||
for (let param of keyValueObj) {
|
||||
params[param.url] = param.filename
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
const handleKeyValueChange = (e: CustomEvent<KeyValuePair[]>) => {
|
||||
const update = {
|
||||
[key]: e.detail.map(({ name, value }) => ({
|
||||
url: name,
|
||||
filename: value,
|
||||
})),
|
||||
}
|
||||
|
||||
dispatch("change", update)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="attachment-field-wrapper">
|
||||
<div class="toggle-container">
|
||||
<Toggle
|
||||
value={useAttachmentBinding}
|
||||
text={"Use bindings"}
|
||||
on:change={e => {
|
||||
dispatch("change", {
|
||||
[key]: null,
|
||||
meta: {
|
||||
useAttachmentBinding: e.detail,
|
||||
},
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="attachment-field-width">
|
||||
{#if !useAttachmentBinding}
|
||||
{#key value}
|
||||
<KeyValueBuilder
|
||||
on:change={handleKeyValueChange}
|
||||
object={handleAttachmentParams(value)}
|
||||
allowJS
|
||||
{bindings}
|
||||
keyBindings
|
||||
customButtonText={buttonText}
|
||||
keyPlaceholder={"URL"}
|
||||
valuePlaceholder={"Filename"}
|
||||
{context}
|
||||
/>
|
||||
{/key}
|
||||
{:else}
|
||||
<DrawerBindableInput
|
||||
panel={AutomationBindingPanel}
|
||||
{value}
|
||||
on:change={e => dispatch("change", { [key]: e.detail })}
|
||||
{bindings}
|
||||
updateOnChange={false}
|
||||
{context}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,122 @@
|
|||
<script lang="ts">
|
||||
import { ActionButton, Drawer, DrawerContent, Button } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { getSchemaForDatasourcePlus } from "@/dataBinding"
|
||||
import { ServerBindingPanel as AutomationBindingPanel } from "@/components/common/bindings"
|
||||
import FilterBuilder from "@/components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||
import { tables, automationStore } from "@/stores/builder"
|
||||
import type {
|
||||
AutomationStep,
|
||||
AutomationTrigger,
|
||||
BaseIOStructure,
|
||||
UISearchFilter,
|
||||
} from "@budibase/types"
|
||||
import { AutomationCustomIOType } from "@budibase/types"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
import { QueryUtils, Utils, search } from "@budibase/frontend-core"
|
||||
import { cloneDeep } from "lodash"
|
||||
import { type DynamicProperties, type StepInputs } from "@/types/automations"
|
||||
|
||||
export let block: AutomationStep | AutomationTrigger | undefined
|
||||
export let context
|
||||
export let bindings
|
||||
export let key
|
||||
|
||||
// Mix in dynamic filter props
|
||||
type DynamicInputs = StepInputs & DynamicProperties
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let drawer: Drawer
|
||||
|
||||
$: inputData = automationStore.actions.getInputData(block)
|
||||
|
||||
$: schemaProperties = Object.entries(block?.schema?.inputs?.properties || {})
|
||||
$: filters = lookForFilters(schemaProperties)
|
||||
$: filterCount =
|
||||
filters?.groups?.reduce((acc: number, group) => {
|
||||
acc = acc += group?.filters?.length || 0
|
||||
return acc
|
||||
}, 0) || 0
|
||||
|
||||
$: tempFilters = cloneDeep(filters)
|
||||
|
||||
$: tableId = inputData && "tableId" in inputData ? inputData.tableId : null
|
||||
|
||||
$: schema = getSchemaForDatasourcePlus(tableId, {
|
||||
searchableSchema: true,
|
||||
}).schema
|
||||
|
||||
$: schemaFields = search.getFields(
|
||||
$tables.list,
|
||||
Object.values(schema || {}),
|
||||
{ allowLinks: false }
|
||||
)
|
||||
|
||||
const lookForFilters = (
|
||||
properties: [string, BaseIOStructure][]
|
||||
): UISearchFilter | undefined => {
|
||||
if (!properties || !inputData) {
|
||||
return
|
||||
}
|
||||
|
||||
let filter: UISearchFilter | undefined
|
||||
|
||||
// Does not appear in the test modal. Check testData
|
||||
const inputs = inputData as DynamicInputs
|
||||
for (let [key, field] of properties) {
|
||||
// need to look for the builder definition (keyed separately, see saveFilters)
|
||||
if (
|
||||
(field.customType === AutomationCustomIOType.FILTERS ||
|
||||
field.customType === AutomationCustomIOType.TRIGGER_FILTER) &&
|
||||
`${key}-def` in inputs
|
||||
) {
|
||||
filter = inputs[`${key}-def`]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return Array.isArray(filter) ? utils.processSearchFilters(filter) : filter
|
||||
}
|
||||
|
||||
function saveFilters(key: string, filters?: UISearchFilter) {
|
||||
const update = filters ? Utils.parseFilter(filters) : filters
|
||||
const query = QueryUtils.buildQuery(update)
|
||||
|
||||
dispatch("change", {
|
||||
[key]: query,
|
||||
[`${key}-def`]: update, // need to store the builder definition in the automation
|
||||
})
|
||||
|
||||
drawer.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionButton fullWidth on:click={drawer.show}>
|
||||
{filterCount > 0 ? "Update Filter" : "No Filter set"}
|
||||
</ActionButton>
|
||||
|
||||
<Drawer
|
||||
bind:this={drawer}
|
||||
title="Filtering"
|
||||
forceModal
|
||||
on:drawerShow={() => {
|
||||
tempFilters = filters
|
||||
}}
|
||||
>
|
||||
<Button cta slot="buttons" on:click={() => saveFilters(key, tempFilters)}>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
<DrawerContent slot="body">
|
||||
<FilterBuilder
|
||||
filters={tempFilters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
panel={AutomationBindingPanel}
|
||||
on:change={e => (tempFilters = e.detail)}
|
||||
evaluationContext={context}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { Label } from "@budibase/bbui"
|
||||
|
||||
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
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
<script lang="ts">
|
||||
import { API } from "@/api"
|
||||
import { tables } from "@/stores/builder"
|
||||
import { type TableDatasource, SortOrder } from "@budibase/types"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
import type TableFetch from "@budibase/frontend-core/src/fetch/TableFetch"
|
||||
import { Select } from "@budibase/bbui"
|
||||
|
||||
export let tableId: string | undefined
|
||||
|
||||
let datasource: TableDatasource
|
||||
let fetch: TableFetch | undefined
|
||||
let rowSearchTerm = ""
|
||||
let selectedRow
|
||||
|
||||
$: primaryDisplay = table?.primaryDisplay
|
||||
|
||||
$: table = tableId
|
||||
? $tables.list.find(table => table._id === tableId)
|
||||
: undefined
|
||||
|
||||
$: if (table && tableId) {
|
||||
datasource = { type: "table", tableId }
|
||||
fetch = createFetch(datasource)
|
||||
}
|
||||
|
||||
$: if (rowSearchTerm && primaryDisplay) {
|
||||
fetch?.update({
|
||||
query: {
|
||||
fuzzy: {
|
||||
[primaryDisplay]: rowSearchTerm || "",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const createFetch = (datasource: TableDatasource) => {
|
||||
if (!datasource || !primaryDisplay) {
|
||||
return
|
||||
}
|
||||
|
||||
return fetchData({
|
||||
API,
|
||||
datasource,
|
||||
options: {
|
||||
sortColumn: primaryDisplay,
|
||||
sortOrder: SortOrder.ASCENDING,
|
||||
query: {
|
||||
fuzzy: {
|
||||
[primaryDisplay]: rowSearchTerm || "",
|
||||
},
|
||||
},
|
||||
limit: 20,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
$: fetchedRows = fetch ? $fetch?.rows : []
|
||||
$: fetchLoading = fetch ? $fetch?.loading : false
|
||||
|
||||
const compare = (a: any, b: any) => {
|
||||
primaryDisplay && a?.[primaryDisplay] === b?.[primaryDisplay]
|
||||
}
|
||||
</script>
|
||||
|
||||
<Select
|
||||
placeholder={"Select a row"}
|
||||
options={fetchedRows}
|
||||
loading={fetchLoading}
|
||||
value={selectedRow}
|
||||
autocomplete={true}
|
||||
getOptionLabel={row => (primaryDisplay ? row?.[primaryDisplay] : "")}
|
||||
{compare}
|
||||
/>
|
|
@ -11,8 +11,6 @@
|
|||
import { FieldType } from "@budibase/types"
|
||||
|
||||
import 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,21 +234,16 @@
|
|||
)
|
||||
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}
|
||||
|
@ -261,39 +256,6 @@
|
|||
{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}
|
||||
</div>
|
||||
</PropField>
|
||||
{/if}
|
||||
|
@ -305,24 +267,30 @@
|
|||
class:empty={Object.is(editableFields, {})}
|
||||
bind:this={popoverAnchor}
|
||||
>
|
||||
<PropField {componentWidth} {fullWidth}>
|
||||
<div class="prop-control-wrap">
|
||||
<ActionButton
|
||||
icon="Add"
|
||||
on:click={() => {
|
||||
customPopover.show()
|
||||
}}
|
||||
disabled={!schemaFields}
|
||||
>Add fields
|
||||
>
|
||||
Edit fields
|
||||
</ActionButton>
|
||||
{#if schemaFields.length}
|
||||
<ActionButton
|
||||
icon="Remove"
|
||||
on:click={() => {
|
||||
dispatch("change", {
|
||||
meta: { fields: {} },
|
||||
row: {},
|
||||
})
|
||||
}}
|
||||
>Clear
|
||||
>
|
||||
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>
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
// Schema UX Types
|
||||
import FilterSelector from "./FilterSelector.svelte"
|
||||
import ExecuteScript from "./ExecuteScript.svelte"
|
||||
import ExecuteScriptV2 from "./ExecuteScriptV2.svelte"
|
||||
import DateSelector from "./DateSelector.svelte"
|
||||
import SchemaSetup from "./SchemaSetup.svelte"
|
||||
import TableSelector from "./TableSelector.svelte"
|
||||
import CronBuilder from "./CronBuilder.svelte"
|
||||
import FieldSelector from "./FieldSelector.svelte"
|
||||
import FileSelector from "./FileSelector.svelte"
|
||||
import RowSelector from "./RowSelector.svelte"
|
||||
import AutomationSelector from "./AutomationSelector.svelte"
|
||||
import PropField from "./PropField.svelte"
|
||||
import QueryParamSelector from "./QueryParamSelector.svelte"
|
||||
import CodeEditorModal from "./CodeEditorModal.svelte"
|
||||
|
||||
export {
|
||||
FilterSelector,
|
||||
ExecuteScriptV2,
|
||||
DateSelector,
|
||||
SchemaSetup,
|
||||
TableSelector,
|
||||
CronBuilder,
|
||||
FieldSelector,
|
||||
FileSelector,
|
||||
RowSelector,
|
||||
AutomationSelector,
|
||||
PropField,
|
||||
QueryParamSelector,
|
||||
CodeEditorModal,
|
||||
// DEPRECATED
|
||||
ExecuteScript,
|
||||
}
|
|
@ -0,0 +1,330 @@
|
|||
import { DrawerBindableInput } from "@/components/common/bindings"
|
||||
import { automationStore, selectedAutomation } from "@/stores/builder"
|
||||
import { Divider, Helpers, Select } from "@budibase/bbui"
|
||||
import {
|
||||
AutomationEventType,
|
||||
AutomationStep,
|
||||
AutomationStepType,
|
||||
AutomationTrigger,
|
||||
BaseIOStructure,
|
||||
isTrigger,
|
||||
Row,
|
||||
} from "@budibase/types"
|
||||
import AutomationBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte"
|
||||
import { RowSelector, TableSelector } from "."
|
||||
import { get, Writable } from "svelte/store"
|
||||
import {
|
||||
type StepInputs,
|
||||
type SchemaConfigProps,
|
||||
type FormUpdate,
|
||||
} from "@/types/automations"
|
||||
import RowFetch from "./RowFetch.svelte"
|
||||
|
||||
export const getInputValue = (inputData: StepInputs, key: string) => {
|
||||
const idxInput = inputData as Record<string, unknown>
|
||||
return idxInput?.[key]
|
||||
}
|
||||
|
||||
// We do not enforce required fields.
|
||||
export const getFieldLabel = (
|
||||
key: string,
|
||||
value: BaseIOStructure,
|
||||
isRequired = false
|
||||
) => {
|
||||
const requiredSuffix = isRequired ? "*" : ""
|
||||
const label = `${
|
||||
value.title || (key === "row" ? "Row" : key)
|
||||
} ${requiredSuffix}`
|
||||
return Helpers.capitalise(label)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a custom step layout. This generates an array of ui elements
|
||||
* intended to be rendered as a uniform column
|
||||
* If you intend to have a completely unique UX,
|
||||
*
|
||||
* @param block
|
||||
* @param stepState
|
||||
* @param isTest
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export const getCustomStepLayout = (
|
||||
block: AutomationStep | AutomationTrigger | undefined,
|
||||
stepState: Writable<Record<string, any>>,
|
||||
isTest = false
|
||||
): SchemaConfigProps[] | undefined => {
|
||||
if (!block) {
|
||||
return
|
||||
}
|
||||
|
||||
// Use the test data for test mode otherwise resolve trigger/step inputs by block
|
||||
const fieldData = isTest
|
||||
? get(selectedAutomation)?.data?.testData
|
||||
: automationStore.actions.getInputData(block)
|
||||
|
||||
const coreDivider = {
|
||||
comp: Divider,
|
||||
props: () => ({
|
||||
noMargin: true,
|
||||
}),
|
||||
wrapped: false,
|
||||
}
|
||||
|
||||
const getIdConfig = (rowIdentifier: string): SchemaConfigProps[] => {
|
||||
const schema =
|
||||
block.type === AutomationStepType.TRIGGER
|
||||
? block.schema.outputs.properties
|
||||
: block.schema.inputs.properties
|
||||
|
||||
const rowIdEntry = schema[rowIdentifier]
|
||||
if (!rowIdEntry) {
|
||||
return []
|
||||
}
|
||||
|
||||
const rowIdlabel = getFieldLabel(rowIdentifier, rowIdEntry)
|
||||
|
||||
const layout: SchemaConfigProps[] = [
|
||||
{
|
||||
comp: DrawerBindableInput,
|
||||
title: rowIdlabel,
|
||||
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||
const update = { [rowIdentifier]: e.detail }
|
||||
if (block) {
|
||||
automationStore.actions.requestUpdate(update, block)
|
||||
}
|
||||
},
|
||||
props: () => {
|
||||
return {
|
||||
panel: AutomationBindingPanel,
|
||||
value: getInputValue(fieldData, rowIdentifier),
|
||||
updateOnChange: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return layout
|
||||
}
|
||||
|
||||
const getRowSelector = (): SchemaConfigProps[] => {
|
||||
const row: Row = getInputValue(fieldData, "row") as Row
|
||||
const meta: Record<string, unknown> = getInputValue(
|
||||
fieldData,
|
||||
"meta"
|
||||
) as Record<string, unknown>
|
||||
|
||||
return [
|
||||
{
|
||||
comp: RowSelector,
|
||||
wrapped: false,
|
||||
props: () => ({
|
||||
row,
|
||||
meta: meta || {},
|
||||
componentWidth: 280,
|
||||
fullWidth: true,
|
||||
}),
|
||||
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||
if (block) {
|
||||
const metaUpdate = e.detail?.meta as Record<string, unknown>
|
||||
|
||||
automationStore.actions.requestUpdate(
|
||||
{
|
||||
row: e.detail.row,
|
||||
meta: {
|
||||
fields: metaUpdate?.fields || {},
|
||||
},
|
||||
},
|
||||
block
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const getTableSelector = (): SchemaConfigProps[] => {
|
||||
const row: Row = getInputValue(fieldData, "row") as Row
|
||||
return [
|
||||
{
|
||||
comp: TableSelector,
|
||||
title: "Table",
|
||||
onChange: (e: CustomEvent) => {
|
||||
const rowKey = get(stepState)[block.id]?.rowType || "row"
|
||||
automationStore.actions.requestUpdate(
|
||||
{
|
||||
_tableId: e.detail,
|
||||
meta: {},
|
||||
[rowKey]: e.detail
|
||||
? {
|
||||
tableId: e.detail,
|
||||
}
|
||||
: {},
|
||||
},
|
||||
block
|
||||
)
|
||||
},
|
||||
props: () => ({
|
||||
isTrigger: isTrigger(block),
|
||||
value: row?.tableId ?? "",
|
||||
disabled: isTest,
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// STEP
|
||||
if (automationStore.actions.isRowStep(block)) {
|
||||
const layout: SchemaConfigProps[] = [
|
||||
...getTableSelector(),
|
||||
...getIdConfig("rowId"),
|
||||
coreDivider,
|
||||
...getRowSelector(),
|
||||
]
|
||||
return layout
|
||||
} else if (automationStore.actions.isRowTrigger(block) && isTest) {
|
||||
// TRIGGER/TEST - Will be used to replace the test modal
|
||||
const schema = block.schema.outputs.properties
|
||||
|
||||
const getRevConfig = (): SchemaConfigProps[] => {
|
||||
// part of outputs for row type
|
||||
const rowRevEntry = schema["revision"]
|
||||
if (!rowRevEntry) {
|
||||
return []
|
||||
}
|
||||
const rowRevlabel = getFieldLabel("revision", rowRevEntry)
|
||||
const selected = get(selectedAutomation)
|
||||
const triggerOutputs = selected.data?.testData
|
||||
|
||||
return [
|
||||
{
|
||||
comp: DrawerBindableInput,
|
||||
title: rowRevlabel,
|
||||
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||
const update = { ["revision"]: e.detail }
|
||||
if (block) {
|
||||
automationStore.actions.requestUpdate(update, block)
|
||||
}
|
||||
},
|
||||
props: () => ({
|
||||
panel: AutomationBindingPanel,
|
||||
value: triggerOutputs ? triggerOutputs["revision"] : undefined,
|
||||
updateOnChange: false,
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// A select to switch from `row` to `oldRow`
|
||||
const getRowTypeConfig = (): SchemaConfigProps[] => {
|
||||
if (block.event !== AutomationEventType.ROW_UPDATE) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!get(stepState)?.[block.id]?.rowType) {
|
||||
stepState.update(state => ({
|
||||
...state,
|
||||
[block.id]: {
|
||||
rowType: "row",
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
type RowType = { name: string; id: string }
|
||||
|
||||
return [
|
||||
{
|
||||
comp: Select,
|
||||
tooltip: `You can configure test data for both the updated row and
|
||||
the old row, if you need it. Just select the one you wish to alter`,
|
||||
title: "Row data",
|
||||
props: () => ({
|
||||
value: get(stepState)?.[block.id].rowType,
|
||||
placeholder: false,
|
||||
getOptionLabel: (type: RowType) => type.name,
|
||||
getOptionValue: (type: RowType) => type.id,
|
||||
options: [
|
||||
{
|
||||
id: "row",
|
||||
name: "Updated row",
|
||||
},
|
||||
{ id: "oldRow", name: "Old row" },
|
||||
] as RowType[],
|
||||
}),
|
||||
onChange: (e: CustomEvent) => {
|
||||
if (e.detail === get(stepState)?.[block.id].rowType) {
|
||||
return
|
||||
}
|
||||
stepState.update(state => ({
|
||||
...state,
|
||||
[block.id]: {
|
||||
rowType: e.detail,
|
||||
},
|
||||
}))
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const getTriggerRowSelector = (): SchemaConfigProps[] => {
|
||||
const rowKey = get(stepState)?.[block.id]?.rowType ?? "row"
|
||||
const rowData: Row = getInputValue(fieldData, rowKey) as Row
|
||||
const row: Row = getInputValue(fieldData, "row") as Row
|
||||
const meta: Record<string, unknown> = getInputValue(
|
||||
fieldData,
|
||||
"meta"
|
||||
) as Record<string, unknown>
|
||||
return [
|
||||
{
|
||||
comp: RowSelector,
|
||||
wrapped: false,
|
||||
props: () => ({
|
||||
componentWidth: 280,
|
||||
row: rowData || {
|
||||
tableId: row?.tableId,
|
||||
},
|
||||
meta: {
|
||||
fields: meta?.oldFields || {},
|
||||
},
|
||||
}),
|
||||
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||
const metaUpdate = e.detail?.meta as Record<string, unknown>
|
||||
|
||||
const update = {
|
||||
[rowKey]: e.detail.row,
|
||||
meta: {
|
||||
fields: meta?.fields || {},
|
||||
oldFields:
|
||||
(metaUpdate?.fields as Record<string, unknown>) || {},
|
||||
},
|
||||
}
|
||||
if (block) {
|
||||
automationStore.actions.requestUpdate(update, block)
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const layout: SchemaConfigProps[] = [
|
||||
...getTableSelector(),
|
||||
{
|
||||
comp: RowFetch,
|
||||
title: "Row",
|
||||
props: () => {
|
||||
return {
|
||||
tableId: "ta_bb_employee",
|
||||
meta: getInputValue(fieldData, "meta"),
|
||||
}
|
||||
},
|
||||
onChange: () => {},
|
||||
},
|
||||
...getIdConfig("id"),
|
||||
...getRevConfig(),
|
||||
...getRowTypeConfig(),
|
||||
coreDivider,
|
||||
...getTriggerRowSelector(),
|
||||
]
|
||||
return layout
|
||||
}
|
||||
}
|
|
@ -7,15 +7,15 @@
|
|||
import { createEventDispatcher } from "svelte"
|
||||
import { 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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()) {
|
||||
const onBlurJSValue = (e: { detail: string }) => {
|
||||
// Don't bother saving empty values as JS
|
||||
updateValue(null)
|
||||
} else {
|
||||
updateValue(encodeJSBinding(e.detail))
|
||||
}
|
||||
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 */
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
keyPlaceholder="Binding name"
|
||||
valuePlaceholder="Default"
|
||||
bindings={[...userBindings]}
|
||||
bindingDrawerLeft="260px"
|
||||
allowHelpers={false}
|
||||
on:change
|
||||
/>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
@ -0,0 +1,213 @@
|
|||
import {
|
||||
type AutomationStep,
|
||||
type AutomationStepInputs,
|
||||
type AutomationTrigger,
|
||||
type AutomationTriggerInputs,
|
||||
type BaseIOStructure,
|
||||
type UISearchFilter,
|
||||
type TestAutomationResponse,
|
||||
type AppSelfResponse,
|
||||
type Automation,
|
||||
type BlockDefinitions,
|
||||
type BlockRef,
|
||||
AutomationActionStepId,
|
||||
AutomationTriggerStepId,
|
||||
AutomationCustomIOType,
|
||||
AutomationIOType,
|
||||
} from "@budibase/types"
|
||||
import { SvelteComponent } from "svelte"
|
||||
|
||||
export enum DataMode {
|
||||
INPUT = "data_in",
|
||||
OUTPUT = "data_out",
|
||||
ERRORS = "errors",
|
||||
}
|
||||
|
||||
export enum SchemaFieldTypes {
|
||||
JSON = "json",
|
||||
ENUM = "enum",
|
||||
BOOL = "boolean",
|
||||
DATE = "date",
|
||||
FILE = "file",
|
||||
FILTER = "filter",
|
||||
CRON = "cron",
|
||||
FIELDS = "fields",
|
||||
TABLE = "table",
|
||||
COLUMN = "column",
|
||||
AUTOMATION_FIELDS = "automation_fields",
|
||||
WEBHOOK_URL = "webhook_url",
|
||||
TRIGGER_SCHEMA = "trigger_schema",
|
||||
LOOP_OPTION = "loop_option",
|
||||
CODE = "code",
|
||||
CODE_V2 = "code_v2",
|
||||
STRING = "string",
|
||||
QUERY_PARAMS = "query_params",
|
||||
QUERY_LIMIT = "query_limit",
|
||||
}
|
||||
|
||||
export type KeyValuePair = {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
// Required for filter fields. The have a definition entry
|
||||
// as well as a configuration
|
||||
export type DynamicProperties = {
|
||||
[property: `${string}-def`]: UISearchFilter
|
||||
}
|
||||
|
||||
// Form field update by property key
|
||||
export type FormUpdate = Record<string, unknown>
|
||||
|
||||
export type AutomationSchemaConfig = Record<SchemaFieldTypes, SchemaConfigProps>
|
||||
|
||||
export type FieldProps = {
|
||||
key: string
|
||||
field: BaseIOStructure
|
||||
block: AutomationStep | AutomationTrigger
|
||||
value: any
|
||||
meta: any
|
||||
title: string
|
||||
}
|
||||
|
||||
// Unused for the moment
|
||||
export type InputMeta = {
|
||||
meta?: AutomationStepInputMeta<AutomationActionStepId>
|
||||
}
|
||||
|
||||
// This is still Attachment Specific and technically reusable.
|
||||
export type FileSelectorMeta = { useAttachmentBinding: boolean }
|
||||
|
||||
export type AutomationStepInputMeta<T extends AutomationActionStepId> =
|
||||
T extends AutomationActionStepId.SEND_EMAIL_SMTP
|
||||
? { meta: FileSelectorMeta }
|
||||
: { meta?: Record<string, unknown> }
|
||||
|
||||
export type StepInputs =
|
||||
| AutomationStepInputs<AutomationActionStepId> //& InputMeta
|
||||
| AutomationTriggerInputs<AutomationTriggerStepId> //& InputMeta
|
||||
| undefined
|
||||
|
||||
export const RowTriggers = [
|
||||
AutomationTriggerStepId.ROW_UPDATED,
|
||||
AutomationTriggerStepId.ROW_SAVED,
|
||||
AutomationTriggerStepId.ROW_DELETED,
|
||||
AutomationTriggerStepId.ROW_ACTION,
|
||||
]
|
||||
|
||||
export const RowSteps = [
|
||||
AutomationActionStepId.CREATE_ROW,
|
||||
AutomationActionStepId.UPDATE_ROW,
|
||||
AutomationActionStepId.DELETE_ROW,
|
||||
]
|
||||
|
||||
export const FilterableRowTriggers = [
|
||||
AutomationTriggerStepId.ROW_UPDATED,
|
||||
AutomationTriggerStepId.ROW_SAVED,
|
||||
]
|
||||
|
||||
/**
|
||||
* Used to define how to represent automation setting
|
||||
* forms
|
||||
*/
|
||||
export interface SchemaConfigProps {
|
||||
comp: typeof SvelteComponent<any>
|
||||
onChange?: (e: CustomEvent) => void
|
||||
props?: (opts?: FieldProps) => Record<string, unknown>
|
||||
fullWidth?: boolean
|
||||
title?: string
|
||||
tooltip?: string
|
||||
wrapped?: boolean
|
||||
}
|
||||
|
||||
export interface AutomationState {
|
||||
automations: Automation[]
|
||||
testResults?: TestAutomationResponse
|
||||
showTestModal: boolean
|
||||
blockDefinitions: BlockDefinitions
|
||||
selectedAutomationId: string | null
|
||||
appSelf?: AppSelfResponse
|
||||
selectedNodeId?: string
|
||||
selectedNodeMode?: DataMode
|
||||
}
|
||||
|
||||
export interface DerivedAutomationState extends AutomationState {
|
||||
data?: Automation
|
||||
blockRefs: Record<string, BlockRef>
|
||||
}
|
||||
|
||||
/**
|
||||
* BlockProperties - Direct mapping of customType to SchemaFieldTypes
|
||||
*/
|
||||
export const customTypeToSchema: Record<string, SchemaFieldTypes> = {
|
||||
[AutomationCustomIOType.TRIGGER_SCHEMA]: SchemaFieldTypes.TRIGGER_SCHEMA,
|
||||
[AutomationCustomIOType.TABLE]: SchemaFieldTypes.TABLE,
|
||||
[AutomationCustomIOType.COLUMN]: SchemaFieldTypes.COLUMN,
|
||||
[AutomationCustomIOType.CRON]: SchemaFieldTypes.CRON,
|
||||
[AutomationCustomIOType.LOOP_OPTION]: SchemaFieldTypes.LOOP_OPTION,
|
||||
[AutomationCustomIOType.AUTOMATION_FIELDS]:
|
||||
SchemaFieldTypes.AUTOMATION_FIELDS,
|
||||
[AutomationCustomIOType.WEBHOOK_URL]: SchemaFieldTypes.WEBHOOK_URL,
|
||||
[AutomationCustomIOType.QUERY_LIMIT]: SchemaFieldTypes.QUERY_LIMIT,
|
||||
["fields"]: SchemaFieldTypes.FIELDS,
|
||||
}
|
||||
|
||||
/**
|
||||
* BlockProperties - Direct mapping of type to SchemaFieldTypes
|
||||
*/
|
||||
export const typeToSchema: Partial<Record<AutomationIOType, SchemaFieldTypes>> =
|
||||
{
|
||||
[AutomationIOType.BOOLEAN]: SchemaFieldTypes.BOOL,
|
||||
[AutomationIOType.DATE]: SchemaFieldTypes.DATE,
|
||||
[AutomationIOType.JSON]: SchemaFieldTypes.JSON,
|
||||
[AutomationIOType.ATTACHMENT]: SchemaFieldTypes.FILE,
|
||||
}
|
||||
|
||||
/**
|
||||
* FlowItem - Status type enums for grading issues
|
||||
*/
|
||||
export enum FlowStatusType {
|
||||
INFO = "info",
|
||||
WARN = "warn",
|
||||
ERROR = "error",
|
||||
SUCCESS = "success",
|
||||
}
|
||||
|
||||
/**
|
||||
* FlowItemStatus - Message structure for building information
|
||||
* pills that hover above the steps in the automation tree
|
||||
*/
|
||||
export type FlowItemStatus = {
|
||||
message: string
|
||||
icon: string
|
||||
type: FlowStatusType
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* BlockData - Status type designation for errors or issues arising from
|
||||
* test runs or other automation concerns
|
||||
*/
|
||||
export enum BlockStatusType {
|
||||
INFO = "info",
|
||||
WARN = "warn",
|
||||
ERROR = "error",
|
||||
}
|
||||
|
||||
/**
|
||||
* BlockData - Source type used to discern the source of the issues
|
||||
*/
|
||||
export enum BlockStatusSource {
|
||||
AUTOMATION_RESULTS = "results",
|
||||
VALIDATION = "validation",
|
||||
}
|
||||
|
||||
/**
|
||||
* BlockData - Message structure for building out issues to be displayed
|
||||
* in the Step Panel in automations
|
||||
*/
|
||||
export type BlockStatus = {
|
||||
message: string
|
||||
type: BlockStatusType
|
||||
source?: BlockStatusSource
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"extends": "./tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./dist",
|
||||
"lib": ["ESNext"],
|
||||
"baseUrl": ".",
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 }}",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -28,7 +28,7 @@ export const definition: AutomationStepDefinition = {
|
|||
},
|
||||
},
|
||||
customType: AutomationCustomIOType.AUTOMATION_FIELDS,
|
||||
title: "automatioFields",
|
||||
title: "Automation Fields",
|
||||
required: ["automationId"],
|
||||
},
|
||||
timeout: {
|
||||
|
|
|
@ -320,6 +320,7 @@ export type WebhookTriggerOutputs = Record<string, any> & {
|
|||
|
||||
export type RowActionTriggerInputs = {
|
||||
tableId: string
|
||||
rowActionId: string
|
||||
}
|
||||
|
||||
export type RowActionTriggerOutputs = {
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue