Merge pull request #12174 from Budibase/feat/automation-naming-ux-updates
Automation naming and UX updates
This commit is contained in:
commit
be73255e3a
|
@ -6,3 +6,4 @@ release/
|
|||
dist/
|
||||
routify
|
||||
.routify/
|
||||
svelte.config.js
|
|
@ -3,6 +3,7 @@ import { API } from "api"
|
|||
import { cloneDeep } from "lodash/fp"
|
||||
import { generate } from "shortid"
|
||||
import { selectedAutomation } from "builderStore"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
const initialAutomationState = {
|
||||
automations: [],
|
||||
|
@ -21,6 +22,37 @@ export const getAutomationStore = () => {
|
|||
return store
|
||||
}
|
||||
|
||||
const updateReferencesInObject = (obj, modifiedIndex, action) => {
|
||||
const regex = /{{\s*steps\.(\d+)\./g
|
||||
for (const key in obj) {
|
||||
if (typeof obj[key] === "string") {
|
||||
let matches
|
||||
while ((matches = regex.exec(obj[key])) !== null) {
|
||||
const referencedStep = parseInt(matches[1])
|
||||
if (action === "add" && referencedStep >= modifiedIndex) {
|
||||
obj[key] = obj[key].replace(
|
||||
`{{ steps.${referencedStep}.`,
|
||||
`{{ steps.${referencedStep + 1}.`
|
||||
)
|
||||
} else if (action === "delete" && referencedStep > modifiedIndex) {
|
||||
obj[key] = obj[key].replace(
|
||||
`{{ steps.${referencedStep}.`,
|
||||
`{{ steps.${referencedStep - 1}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
||||
updateReferencesInObject(obj[key], modifiedIndex, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateStepReferences = (steps, modifiedIndex, action) => {
|
||||
steps.forEach(step => {
|
||||
updateReferencesInObject(step.inputs, modifiedIndex, action)
|
||||
})
|
||||
}
|
||||
|
||||
const automationActions = store => ({
|
||||
definitions: async () => {
|
||||
const response = await API.getAutomationDefinitions()
|
||||
|
@ -218,10 +250,40 @@ const automationActions = store => ({
|
|||
if (!automation) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
updateStepReferences(newAutomation.definition.steps, blockIdx, "add")
|
||||
} catch (e) {
|
||||
notifications.error("Error adding automation block")
|
||||
}
|
||||
newAutomation.definition.steps.splice(blockIdx, 0, block)
|
||||
await store.actions.save(newAutomation)
|
||||
},
|
||||
deleteAutomationBlock: async block => {
|
||||
saveAutomationName: async (blockId, name) => {
|
||||
const automation = get(selectedAutomation)
|
||||
let newAutomation = cloneDeep(automation)
|
||||
if (!automation) {
|
||||
return
|
||||
}
|
||||
newAutomation.definition.stepNames = {
|
||||
...newAutomation.definition.stepNames,
|
||||
[blockId]: name.trim(),
|
||||
}
|
||||
|
||||
await store.actions.save(newAutomation)
|
||||
},
|
||||
deleteAutomationName: async blockId => {
|
||||
const automation = get(selectedAutomation)
|
||||
let newAutomation = cloneDeep(automation)
|
||||
if (!automation) {
|
||||
return
|
||||
}
|
||||
delete newAutomation.definition.stepNames[blockId]
|
||||
|
||||
await store.actions.save(newAutomation)
|
||||
},
|
||||
|
||||
deleteAutomationBlock: async (block, blockIdx) => {
|
||||
const automation = get(selectedAutomation)
|
||||
let newAutomation = cloneDeep(automation)
|
||||
|
||||
|
@ -233,7 +295,14 @@ const automationActions = store => ({
|
|||
newAutomation.definition.steps = newAutomation.definition.steps.filter(
|
||||
step => step.id !== block.id
|
||||
)
|
||||
delete newAutomation.definition.stepNames?.[block.id]
|
||||
}
|
||||
try {
|
||||
updateStepReferences(newAutomation.definition.steps, blockIdx, "delete")
|
||||
} catch (e) {
|
||||
notifications.error("Error deleting automation block")
|
||||
}
|
||||
|
||||
await store.actions.save(newAutomation)
|
||||
},
|
||||
replace: async (automationId, automation) => {
|
||||
|
|
|
@ -5,13 +5,7 @@
|
|||
import TestDataModal from "./TestDataModal.svelte"
|
||||
import { flip } from "svelte/animate"
|
||||
import { fly } from "svelte/transition"
|
||||
import {
|
||||
Heading,
|
||||
Icon,
|
||||
ActionButton,
|
||||
notifications,
|
||||
Modal,
|
||||
} from "@budibase/bbui"
|
||||
import { Icon, notifications, Modal } from "@budibase/bbui"
|
||||
import { ActionStepID } from "constants/backend/automations"
|
||||
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
|
||||
import { automationHistoryStore } from "builderStore"
|
||||
|
@ -20,9 +14,8 @@
|
|||
|
||||
let testDataModal
|
||||
let confirmDeleteDialog
|
||||
|
||||
$: blocks = getBlocks(automation)
|
||||
|
||||
let scrolling = false
|
||||
$: blocks = getBlocks(automation).filter(x => x.stepId !== ActionStepID.LOOP)
|
||||
const getBlocks = automation => {
|
||||
let blocks = []
|
||||
if (automation.definition.trigger) {
|
||||
|
@ -32,46 +25,59 @@
|
|||
return blocks
|
||||
}
|
||||
|
||||
async function deleteAutomation() {
|
||||
const deleteAutomation = async () => {
|
||||
try {
|
||||
await automationStore.actions.delete($selectedAutomation)
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting automation")
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = e => {
|
||||
if (e.target.scrollTop >= 30) {
|
||||
scrolling = true
|
||||
} else if (e.target.scrollTop) {
|
||||
// Set scrolling back to false if scrolled back to less than 100px
|
||||
scrolling = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="canvas">
|
||||
<div class="header">
|
||||
<Heading size="S">{automation.name}</Heading>
|
||||
<div class="controls">
|
||||
<div class="header" class:scrolling>
|
||||
<div class="header-left">
|
||||
<UndoRedoControl store={automationHistoryStore} />
|
||||
<Icon
|
||||
on:click={confirmDeleteDialog.show}
|
||||
hoverable
|
||||
size="M"
|
||||
name="DeleteOutline"
|
||||
/>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="buttons">
|
||||
<ActionButton
|
||||
<Icon hoverable size="M" name="Play" />
|
||||
<div
|
||||
on:click={() => {
|
||||
testDataModal.show()
|
||||
}}
|
||||
icon="MultipleCheck"
|
||||
size="M">Run test</ActionButton
|
||||
>
|
||||
<ActionButton
|
||||
Run test
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<Icon
|
||||
disabled={!$automationStore.testResults}
|
||||
hoverable
|
||||
size="M"
|
||||
name="Multiple"
|
||||
/>
|
||||
<div
|
||||
class:disabled={!$automationStore.testResults}
|
||||
on:click={() => {
|
||||
$automationStore.showTestPanel = true
|
||||
}}
|
||||
size="M">Test Details</ActionButton
|
||||
>
|
||||
Test details
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="canvas" on:scroll={handleScroll}>
|
||||
<div class="content">
|
||||
{#each blocks as block, idx (block.id)}
|
||||
<div
|
||||
class="block"
|
||||
|
@ -84,6 +90,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
|
@ -103,6 +110,12 @@
|
|||
<style>
|
||||
.canvas {
|
||||
padding: var(--spacing-l) var(--spacing-xl);
|
||||
overflow-y: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.header-left :global(div) {
|
||||
border-right: none;
|
||||
}
|
||||
/* Fix for firefox not respecting bottom padding in scrolling containers */
|
||||
.canvas > *:last-child {
|
||||
|
@ -117,23 +130,45 @@
|
|||
}
|
||||
|
||||
.content {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
flex-grow: 1;
|
||||
padding: 23px 23px 80px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header.scrolling {
|
||||
background: var(--background);
|
||||
border-bottom: var(--border-light);
|
||||
border-left: var(--border-light);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: var(--spacing-l);
|
||||
transition: background 130ms ease-out;
|
||||
flex: 0 0 48px;
|
||||
padding-right: var(--spacing-xl);
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.controls,
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.buttons {
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.buttons:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--spectrum-global-color-gray-500) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -7,20 +7,16 @@
|
|||
Detail,
|
||||
Modal,
|
||||
Button,
|
||||
ActionButton,
|
||||
notifications,
|
||||
Label,
|
||||
AbsTooltip,
|
||||
} from "@budibase/bbui"
|
||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||
import ActionModal from "./ActionModal.svelte"
|
||||
import FlowItemHeader from "./FlowItemHeader.svelte"
|
||||
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
||||
import {
|
||||
ActionStepID,
|
||||
TriggerStepID,
|
||||
Features,
|
||||
} from "constants/backend/automations"
|
||||
import { ActionStepID, TriggerStepID } from "constants/backend/automations"
|
||||
import { permissions } from "stores/backend"
|
||||
|
||||
export let block
|
||||
|
@ -86,7 +82,7 @@
|
|||
if (loopBlock) {
|
||||
await automationStore.actions.deleteAutomationBlock(loopBlock)
|
||||
}
|
||||
await automationStore.actions.deleteAutomationBlock(block)
|
||||
await automationStore.actions.deleteAutomationBlock(block, blockIdx)
|
||||
} catch (error) {
|
||||
notifications.error("Error saving automation")
|
||||
}
|
||||
|
@ -129,6 +125,10 @@
|
|||
</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>
|
||||
|
@ -139,9 +139,6 @@
|
|||
<Divider noMargin />
|
||||
{#if !showLooping}
|
||||
<div class="blockSection">
|
||||
<div class="block-options">
|
||||
<ActionButton on:click={() => removeLooping()} icon="DeleteOutline" />
|
||||
</div>
|
||||
<Layout noPadding gap="S">
|
||||
<AutomationBlockSetup
|
||||
schemaProperties={Object.entries(
|
||||
|
@ -162,31 +159,19 @@
|
|||
{block}
|
||||
{testDataModal}
|
||||
{idx}
|
||||
{addLooping}
|
||||
{deleteStep}
|
||||
on:toggle={() => (open = !open)}
|
||||
/>
|
||||
{#if open}
|
||||
<Divider noMargin />
|
||||
<div class="blockSection">
|
||||
<Layout noPadding gap="S">
|
||||
{#if !isTrigger}
|
||||
<div>
|
||||
<div class="block-options">
|
||||
{#if !loopBlock && (block?.features?.[Features.LOOPING] || !block.features)}
|
||||
<ActionButton on:click={() => addLooping()} icon="Reuse">
|
||||
Add Looping
|
||||
</ActionButton>
|
||||
{/if}
|
||||
<ActionButton
|
||||
on:click={() => deleteStep()}
|
||||
icon="DeleteOutline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isAppAction}
|
||||
<div>
|
||||
<Label>Role</Label>
|
||||
<RoleSelect bind:value={role} />
|
||||
</div>
|
||||
{/if}
|
||||
<AutomationBlockSetup
|
||||
schemaProperties={Object.entries(block.schema.inputs.properties)}
|
||||
|
@ -270,5 +255,6 @@
|
|||
.blockTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<script>
|
||||
import { automationStore } from "builderStore"
|
||||
import { Icon, Body, Detail, StatusLight } from "@budibase/bbui"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import { Icon, Body, StatusLight, AbsTooltip } from "@budibase/bbui"
|
||||
import { externalActions } from "./ExternalActions"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { Features } from "constants/backend/automations"
|
||||
|
||||
export let block
|
||||
export let open
|
||||
|
@ -10,9 +11,20 @@
|
|||
export let testResult
|
||||
export let isTrigger
|
||||
export let idx
|
||||
export let addLooping
|
||||
export let deleteStep
|
||||
|
||||
let validRegex = /^[A-Za-z0-9_\s]+$/
|
||||
let typing = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: stepNames = $selectedAutomation.definition.stepNames
|
||||
$: automationName = stepNames?.[block.id] || block?.name || ""
|
||||
$: automationNameError = getAutomationNameError(automationName)
|
||||
$: status = updateStatus(testResult, isTrigger)
|
||||
$: isHeaderTrigger = isTrigger || block.type === "TRIGGER"
|
||||
|
||||
$: {
|
||||
if (!testResult) {
|
||||
testResult = $automationStore.testResults?.steps?.filter(step =>
|
||||
|
@ -20,8 +32,9 @@
|
|||
)?.[0]
|
||||
}
|
||||
}
|
||||
$: isTrigger = isTrigger || block.type === "TRIGGER"
|
||||
$: status = updateStatus(testResult, isTrigger)
|
||||
$: loopBlock = $selectedAutomation.definition.steps.find(
|
||||
x => x.blockToLoop === block?.id
|
||||
)
|
||||
|
||||
async function onSelect(block) {
|
||||
await automationStore.update(state => {
|
||||
|
@ -43,10 +56,47 @@
|
|||
return { negative: true, message: "Error" }
|
||||
}
|
||||
}
|
||||
|
||||
const getAutomationNameError = name => {
|
||||
for (const [key, value] of Object.entries(stepNames)) {
|
||||
if (name === value && key !== block.id) {
|
||||
return "This name already exists, please enter a unique name"
|
||||
}
|
||||
}
|
||||
|
||||
if (name !== block.name && name?.length > 0) {
|
||||
let invalidRoleName = !validRegex.test(name)
|
||||
if (invalidRoleName) {
|
||||
return "Please enter a role name consisting of only alphanumeric symbols and underscores"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const startTyping = async () => {
|
||||
typing = true
|
||||
}
|
||||
|
||||
const saveName = async () => {
|
||||
if (automationNameError || block.name === automationName) {
|
||||
return
|
||||
}
|
||||
|
||||
if (automationName.length === 0) {
|
||||
await automationStore.actions.deleteAutomationName(block.id)
|
||||
} else {
|
||||
await automationStore.actions.saveAutomationName(block.id, automationName)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="blockSection">
|
||||
<div on:click={() => dispatch("toggle")} class="splitHeader">
|
||||
<div
|
||||
class:typing={typing && !automationNameError}
|
||||
class:typing-error={automationNameError}
|
||||
class="blockSection"
|
||||
>
|
||||
<div class="splitHeader">
|
||||
<div class="center-items">
|
||||
{#if externalActions[block.stepId]}
|
||||
<img
|
||||
|
@ -67,40 +117,104 @@
|
|||
</svg>
|
||||
{/if}
|
||||
<div class="iconAlign">
|
||||
{#if isTrigger}
|
||||
{#if isHeaderTrigger}
|
||||
<Body size="XS"><b>Trigger</b></Body>
|
||||
<Body size="XS">When this happens:</Body>
|
||||
{:else}
|
||||
<div style="margin-left: 2px;">
|
||||
<Body size="XS"><b>Step {idx}</b></Body>
|
||||
<Body size="XS">Do this:</Body>
|
||||
</div>
|
||||
{/if}
|
||||
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
|
||||
<input
|
||||
placeholder="Enter some text"
|
||||
name="name"
|
||||
autocomplete="off"
|
||||
value={automationName}
|
||||
on:input={e => {
|
||||
automationName = e.target.value.trim()
|
||||
}}
|
||||
on:click={startTyping}
|
||||
on:blur={async () => {
|
||||
typing = false
|
||||
if (automationNameError) {
|
||||
automationName = stepNames[block.id] || block?.name
|
||||
} else {
|
||||
await saveName()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="blockTitle">
|
||||
{#if showTestStatus && testResult}
|
||||
<div style="float: right;">
|
||||
<div class="status-container">
|
||||
<div style="float:right;">
|
||||
<StatusLight
|
||||
positive={status?.positive}
|
||||
yellow={status?.yellow}
|
||||
negative={status?.negative}
|
||||
><Body size="XS">{status?.message}</Body></StatusLight
|
||||
>
|
||||
<Body size="XS">{status?.message}</Body>
|
||||
</StatusLight>
|
||||
</div>
|
||||
<Icon
|
||||
on:click={() => dispatch("toggle")}
|
||||
hoverable
|
||||
name={open ? "ChevronUp" : "ChevronDown"}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
style="margin-left: 10px; margin-bottom: var(--spacing-xs);"
|
||||
class="context-actions"
|
||||
class:hide-context-actions={typing}
|
||||
on:click={() => {
|
||||
onSelect(block)
|
||||
}}
|
||||
>
|
||||
<Icon hoverable name={open ? "ChevronUp" : "ChevronDown"} />
|
||||
{#if !showTestStatus}
|
||||
{#if !isHeaderTrigger && !loopBlock && (block?.features?.[Features.LOOPING] || !block.features)}
|
||||
<AbsTooltip type="info" text="Add looping">
|
||||
<Icon on:click={addLooping} hoverable name="RotateCW" />
|
||||
</AbsTooltip>
|
||||
{/if}
|
||||
<AbsTooltip type="negative" text="Delete step">
|
||||
<Icon on:click={deleteStep} hoverable name="DeleteOutline" />
|
||||
</AbsTooltip>
|
||||
{/if}
|
||||
{#if !showTestStatus}
|
||||
<Icon
|
||||
on:click={() => dispatch("toggle")}
|
||||
hoverable
|
||||
name={open ? "ChevronUp" : "ChevronDown"}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if automationNameError}
|
||||
<div class="error-container">
|
||||
<AbsTooltip type="negative" text={automationNameError}>
|
||||
<div class="error-icon">
|
||||
<Icon size="S" name="Alert" />
|
||||
</div>
|
||||
</AbsTooltip>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.status-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-m);
|
||||
/* You can also add padding or margin to adjust the spacing between the text and the chevron if needed. */
|
||||
}
|
||||
|
||||
.context-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
.center-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -117,10 +231,55 @@
|
|||
|
||||
.blockSection {
|
||||
padding: var(--spacing-xl);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.blockTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hide-context-actions {
|
||||
display: none;
|
||||
}
|
||||
input {
|
||||
font-family: var(--font-sans);
|
||||
color: var(--ink);
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
font-size: var(--spectrum-alias-font-size-default);
|
||||
width: 230px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Hide arrows for number fields */
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.typing {
|
||||
border: 1px solid var(--spectrum-global-color-static-blue-500);
|
||||
border-radius: 4px 4px 4px 4px;
|
||||
}
|
||||
|
||||
.typing-error {
|
||||
border: 1px solid var(--spectrum-global-color-static-red-500);
|
||||
border-radius: 4px 4px 4px 4px;
|
||||
}
|
||||
|
||||
.error-icon :global(.spectrum-Icon) {
|
||||
fill: var(--spectrum-global-color-red-400);
|
||||
}
|
||||
|
||||
.error-container {
|
||||
padding-top: var(--spacing-xl);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
<ModalContent
|
||||
title="Add test data"
|
||||
confirmText="Test"
|
||||
size="M"
|
||||
showConfirmButton={true}
|
||||
disabled={isError}
|
||||
onConfirm={testAutomation}
|
||||
|
|
|
@ -58,7 +58,6 @@
|
|||
let fillWidth = true
|
||||
let inputData
|
||||
let codeBindingOpen = false
|
||||
|
||||
$: filters = lookForFilters(schemaProperties) || []
|
||||
$: tempFilters = filters
|
||||
$: stepId = block.stepId
|
||||
|
@ -155,7 +154,7 @@
|
|||
}
|
||||
let blockIdx = allSteps.findIndex(step => step.id === block.id)
|
||||
|
||||
// Extract all outputs from all previous steps as available bindins
|
||||
// Extract all outputs from all previous steps as available bindingsx§x
|
||||
let bindings = []
|
||||
let loopBlockCount = 0
|
||||
for (let idx = 0; idx < blockIdx; idx++) {
|
||||
|
@ -183,20 +182,19 @@
|
|||
}
|
||||
}
|
||||
const outputs = Object.entries(schema)
|
||||
|
||||
let bindingIcon = ""
|
||||
let bindindingRank = 0
|
||||
|
||||
let bindingRank = 0
|
||||
if (idx === 0) {
|
||||
bindingIcon = automation.trigger.icon
|
||||
} else if (isLoopBlock) {
|
||||
bindingIcon = "Reuse"
|
||||
bindindingRank = idx + 1
|
||||
bindingRank = idx + 1
|
||||
} else {
|
||||
bindingIcon = allSteps[idx].icon
|
||||
bindindingRank = idx - loopBlockCount
|
||||
bindingRank = idx - loopBlockCount
|
||||
}
|
||||
|
||||
let bindingName =
|
||||
automation.stepNames?.[allSteps[idx - loopBlockCount].id]
|
||||
bindings = bindings.concat(
|
||||
outputs.map(([name, value]) => {
|
||||
let runtimeName = isLoopBlock
|
||||
|
@ -205,14 +203,20 @@
|
|||
? `steps[${idx - loopBlockCount}].${name}`
|
||||
: `steps.${idx - loopBlockCount}.${name}`
|
||||
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
|
||||
const categoryName =
|
||||
idx === 0
|
||||
? "Trigger outputs"
|
||||
: isLoopBlock
|
||||
? "Loop Outputs"
|
||||
: `Step ${idx - loopBlockCount} outputs`
|
||||
|
||||
let categoryName
|
||||
if (idx === 0) {
|
||||
categoryName = "Trigger outputs"
|
||||
} else if (isLoopBlock) {
|
||||
categoryName = "Loop Outputs"
|
||||
} else if (bindingName) {
|
||||
categoryName = `${bindingName} outputs`
|
||||
} else {
|
||||
categoryName = `Step ${idx - loopBlockCount} outputs`
|
||||
}
|
||||
|
||||
return {
|
||||
readableBinding: runtime,
|
||||
readableBinding: bindingName ? `${bindingName}.${name}` : runtime,
|
||||
runtimeBinding: runtime,
|
||||
type: value.type,
|
||||
description: value.description,
|
||||
|
@ -221,7 +225,7 @@
|
|||
display: {
|
||||
type: value.type,
|
||||
name: name,
|
||||
rank: bindindingRank,
|
||||
rank: bindingRank,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -277,6 +281,16 @@
|
|||
return !dependsOn || !!inputData[dependsOn]
|
||||
}
|
||||
|
||||
function shouldRenderField(value) {
|
||||
return (
|
||||
value.customType !== "row" &&
|
||||
value.customType !== "code" &&
|
||||
value.customType !== "queryParams" &&
|
||||
value.customType !== "cron" &&
|
||||
value.customType !== "triggerSchema"
|
||||
)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await environment.loadVariables()
|
||||
|
@ -289,21 +303,23 @@
|
|||
<div class="fields">
|
||||
{#each schemaProperties as [key, value]}
|
||||
{#if canShowField(key, value)}
|
||||
<div class="block-field">
|
||||
{#if key !== "fields" && value.type !== "boolean"}
|
||||
<div class:block-field={shouldRenderField(value)}>
|
||||
{#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)}
|
||||
<Label
|
||||
tooltip={value.title === "Binding / Value"
|
||||
? "If using the String input type, please use a comma or newline separated string"
|
||||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
||||
>
|
||||
{/if}
|
||||
<div class:field-width={shouldRenderField(value)}>
|
||||
{#if value.type === "string" && value.enum && canShowField(key, value)}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
placeholder={false}
|
||||
options={value.enum}
|
||||
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
||||
getOptionLabel={(x, idx) =>
|
||||
value.pretty ? value.pretty[idx] : x}
|
||||
/>
|
||||
{:else if value.type === "json"}
|
||||
<Editor
|
||||
|
@ -529,6 +545,7 @@
|
|||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -541,6 +558,10 @@
|
|||
{/if}
|
||||
|
||||
<style>
|
||||
.field-width {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.messaging {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -555,8 +576,13 @@
|
|||
}
|
||||
|
||||
.block-field {
|
||||
display: grid;
|
||||
grid-gap: 5px;
|
||||
display: flex; /* Use Flexbox */
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row; /* Arrange label and field side by side */
|
||||
align-items: center; /* Align vertically in the center */
|
||||
gap: 10px; /* Add some space between label and field */
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.test :global(.drawer) {
|
||||
|
|
|
@ -23,7 +23,9 @@
|
|||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Button primary on:click={show}>Edit Code</Button>
|
||||
<div class="center">
|
||||
<Button primary on:click={show}>Edit Code</Button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container :global(section > header) {
|
||||
|
@ -33,4 +35,9 @@
|
|||
.container :global(textarea) {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { queries } from "stores/backend"
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
|
||||
|
@ -27,41 +27,55 @@
|
|||
$: if (value?.queryId == null) value = { queryId: "" }
|
||||
</script>
|
||||
|
||||
<div class="block-field">
|
||||
<div class="schema-fields">
|
||||
<Label>Query</Label>
|
||||
<div class="field-width">
|
||||
<Select
|
||||
label="Query"
|
||||
on:change={onChangeQuery}
|
||||
value={value.queryId}
|
||||
options={$queries.list}
|
||||
getOptionValue={query => query._id}
|
||||
getOptionLabel={query => query.name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if parameters.length}
|
||||
<div class="schema-fields">
|
||||
{#each parameters as field}
|
||||
<Label>{field.name}</Label>
|
||||
<div class="field-width">
|
||||
<DrawerBindableInput
|
||||
panel={AutomationBindingPanel}
|
||||
extraThin
|
||||
value={value[field.name]}
|
||||
on:change={e => onChange(e, field)}
|
||||
label={field.name}
|
||||
type="string"
|
||||
{bindings}
|
||||
fillWidth={true}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.schema-fields {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-xl);
|
||||
margin-top: var(--spacing-xl);
|
||||
.field-width {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.schema-fields {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.schema-fields :global(label) {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<script>
|
||||
import { tables } from "stores/backend"
|
||||
import { Select, Checkbox } from "@budibase/bbui"
|
||||
import { Select, Checkbox, Label } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
import { TableNames } from "constants"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -99,16 +100,23 @@
|
|||
$: if (value?.tableId == null) value = { tableId: "" }
|
||||
</script>
|
||||
|
||||
<Select
|
||||
<div class="schema-fields">
|
||||
<Label>Table</Label>
|
||||
<div class="field-width">
|
||||
<Select
|
||||
on:change={onChangeTable}
|
||||
value={value.tableId}
|
||||
options={$tables.list}
|
||||
options={$tables.list.filter(table => table._id !== TableNames.USERS)}
|
||||
getOptionLabel={table => table.name}
|
||||
getOptionValue={table => table._id}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if schemaFields.length}
|
||||
<div class="schema-fields">
|
||||
{#each schemaFields as [field, schema]}
|
||||
<div class="schema-fields">
|
||||
<Label>{field}</Label>
|
||||
<div class="field-width">
|
||||
{#if !schema.autocolumn && schema.type !== "attachment"}
|
||||
{#if isTestModal}
|
||||
<RowSelectorTypes
|
||||
|
@ -123,7 +131,6 @@
|
|||
<DrawerBindableSlot
|
||||
fillWidth
|
||||
title={value.title}
|
||||
label={field}
|
||||
panel={AutomationBindingPanel}
|
||||
type={schema.type}
|
||||
{schema}
|
||||
|
@ -145,6 +152,7 @@
|
|||
</DrawerBindableSlot>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if isUpdateRow && schema.type === "link"}
|
||||
<div class="checkbox-field">
|
||||
<Checkbox
|
||||
|
@ -155,15 +163,25 @@
|
|||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.field-width {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.schema-fields {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-s);
|
||||
margin-top: var(--spacing-s);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.schema-fields :global(label) {
|
||||
text-transform: capitalize;
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
<script>
|
||||
import {
|
||||
Select,
|
||||
DatePicker,
|
||||
Multiselect,
|
||||
TextArea,
|
||||
Label,
|
||||
} from "@budibase/bbui"
|
||||
import { Select, DatePicker, Multiselect, TextArea } from "@budibase/bbui"
|
||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
|
||||
|
@ -33,20 +27,14 @@
|
|||
{#if schemaHasOptions(schema) && schema.type !== "array"}
|
||||
<Select
|
||||
on:change={e => onChange(e, field)}
|
||||
label={field}
|
||||
value={value[field]}
|
||||
options={schema.constraints.inclusion}
|
||||
/>
|
||||
{:else if schema.type === "datetime"}
|
||||
<DatePicker
|
||||
label={field}
|
||||
value={value[field]}
|
||||
on:change={e => onChange(e, field)}
|
||||
/>
|
||||
<DatePicker value={value[field]} on:change={e => onChange(e, field)} />
|
||||
{:else if schema.type === "boolean"}
|
||||
<Select
|
||||
on:change={e => onChange(e, field)}
|
||||
label={field}
|
||||
value={value[field]}
|
||||
options={[
|
||||
{ label: "True", value: "true" },
|
||||
|
@ -56,19 +44,13 @@
|
|||
{:else if schema.type === "array"}
|
||||
<Multiselect
|
||||
bind:value={value[field]}
|
||||
label={field}
|
||||
options={schema.constraints.inclusion}
|
||||
on:change={e => onChange(e, field)}
|
||||
/>
|
||||
{:else if schema.type === "longform"}
|
||||
<TextArea
|
||||
label={field}
|
||||
bind:value={value[field]}
|
||||
on:change={e => onChange(e, field)}
|
||||
/>
|
||||
<TextArea bind:value={value[field]} on:change={e => onChange(e, field)} />
|
||||
{:else if schema.type === "json"}
|
||||
<span>
|
||||
<Label>{field}</Label>
|
||||
<Editor
|
||||
editorHeight="150"
|
||||
mode="json"
|
||||
|
@ -92,7 +74,6 @@
|
|||
panel={AutomationBindingPanel}
|
||||
value={value[field]}
|
||||
on:change={e => onChange(e, field)}
|
||||
label={field}
|
||||
type="string"
|
||||
bindings={parsedBindings}
|
||||
fillWidth={true}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<Select
|
||||
on:change={onChange}
|
||||
bind:value
|
||||
options={filteredTables}
|
||||
options={filteredTables.filter(table => table._id !== TableNames.USERS)}
|
||||
getOptionLabel={table => table.name}
|
||||
getOptionValue={table => table._id}
|
||||
/>
|
||||
|
|
|
@ -206,12 +206,12 @@
|
|||
.text-area-slot-icon {
|
||||
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
||||
border-bottom-right-radius: 0px !important;
|
||||
top: 26px !important;
|
||||
top: 1px !important;
|
||||
}
|
||||
.json-slot-icon {
|
||||
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
||||
border-bottom-right-radius: 0px !important;
|
||||
top: 23px !important;
|
||||
top: 1px !important;
|
||||
right: 0px !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -91,7 +91,6 @@
|
|||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-l);
|
||||
overflow: auto;
|
||||
}
|
||||
.centered {
|
||||
|
|
Loading…
Reference in New Issue