Merge branch 'master' into refresh-all-datasources-peek

This commit is contained in:
deanhannigan 2025-05-09 17:06:05 +01:00 committed by GitHub
commit e1a1ba4936
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 510 additions and 358 deletions

View File

@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.10.2",
"version": "3.10.4",
"npmClient": "yarn",
"concurrency": 20,
"command": {

View File

@ -1,20 +1,20 @@
<script>
<script lang="ts">
import Field from "./Field.svelte"
import Search from "./Core/Search.svelte"
import { createEventDispatcher } from "svelte"
export let value = null
export let label = null
export let labelPosition = "above"
export let placeholder = null
export let value: string | undefined = undefined
export let label: string | undefined = undefined
export let labelPosition: "above" | "below" = "above"
export let placeholder: string | undefined = undefined
export let disabled = false
export let updateOnChange = true
export let quiet = false
export let inputRef = undefined
export let helpText = null
export let inputRef: HTMLInputElement | undefined = undefined
export let helpText: string | undefined = undefined
const dispatch = createEventDispatcher()
const onChange = e => {
const onChange = (e: CustomEvent<string>) => {
value = e.detail
dispatch("change", e.detail)
}

View File

@ -1,288 +0,0 @@
<script lang="ts">
import {
ModalContent,
Layout,
Detail,
Body,
Icon,
notifications,
Tags,
Tag,
} from "@budibase/bbui"
import { AutomationActionStepId, BlockDefinitionTypes } from "@budibase/types"
import { automationStore, selectedAutomation } from "@/stores/builder"
import { admin, licensing } from "@/stores/portal"
import { externalActions } from "./ExternalActions"
import { TriggerStepID, ActionStepID } from "@/constants/backend/automations"
import type { AutomationStepDefinition } from "@budibase/types"
export let block
export let modal
let syncAutomationsEnabled = $licensing.syncAutomationsEnabled
let triggerAutomationRunEnabled = $licensing.triggerAutomationRunEnabled
let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK]
let selectedAction: string | undefined
let actions = Object.entries($automationStore.blockDefinitions.ACTION).filter(
([key, action]) => {
return key !== AutomationActionStepId.BRANCH && action.deprecated !== true
}
)
let lockedFeatures = [
ActionStepID.COLLECT,
ActionStepID.TRIGGER_AUTOMATION_RUN,
]
$: blockRef = $selectedAutomation.blockRefs?.[block.id]
$: lastStep = blockRef?.terminating
$: pathSteps =
block.id && $selectedAutomation?.data
? automationStore.actions.getPathSteps(
blockRef.pathTo,
$selectedAutomation.data
)
: []
$: collectBlockExists = pathSteps?.some(
step => step.stepId === ActionStepID.COLLECT
)
const disabled = () => {
return {
SEND_EMAIL_SMTP: {
disabled:
!$admin.checklist?.smtp?.checked || $admin.checklist?.smtp.fallback,
message: "Please configure SMTP",
},
COLLECT: {
disabled: !lastStep || !syncAutomationsEnabled || collectBlockExists,
message: collectDisabledMessage(),
},
TRIGGER_AUTOMATION_RUN: {
disabled: !triggerAutomationRunEnabled,
message: "Please upgrade to a paid plan",
},
}
}
const checkDisabled = (idx: string) => {
const disabledActions = disabled()
return disabledActions[idx as keyof typeof disabledActions]
}
const collectDisabledMessage = () => {
if (collectBlockExists) {
return "Only one Collect step allowed"
}
if (!lastStep) {
return "Only available as the last step"
}
}
const external = actions.reduce(
(acc: Record<string, AutomationStepDefinition>, elm) => {
const [k, v] = elm
if (!v.internal && !v.custom) {
acc[k] = v
}
return acc
},
{}
)
const internal = actions.reduce(
(acc: Record<string, AutomationStepDefinition>, elm) => {
const [k, v] = elm
if (v.internal) {
acc[k] = v
}
delete acc.LOOP
const stepId = $selectedAutomation.data?.definition.trigger.stepId
// Filter out Collect block if not App Action or Webhook
if (stepId && !collectBlockAllowedSteps.includes(stepId)) {
delete acc.COLLECT
}
return acc
},
{}
)
const plugins = actions.reduce(
(acc: Record<string, AutomationStepDefinition>, elm) => {
const [k, v] = elm
if (v.custom) {
acc[k] = v
}
return acc
},
{}
)
const selectAction = async (action: AutomationStepDefinition) => {
selectedAction = action.name
try {
const newBlock = automationStore.actions.constructBlock(
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)
notifications.error("Error saving automation")
}
}
const getExternalAction = (stepId: string) => {
return externalActions[stepId as keyof typeof externalActions]
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<ModalContent
title="Add automation step"
size="L"
showConfirmButton={false}
showCancelButton={false}
disabled={!selectedAction}
>
<Layout noPadding gap="XS">
<Detail size="S">Apps</Detail>
<div class="item-list">
{#each Object.entries(external) as [idx, action]}
<div
class="item"
class:selected={selectedAction === action.name}
on:click={() => selectAction(action)}
>
<div class="item-body">
<img
width={20}
height={20}
src={getExternalAction(action.stepId)?.icon}
alt={getExternalAction(action.stepId)?.name}
/>
<span class="icon-spacing">
<Body size="XS">
{action.stepTitle || idx.charAt(0).toUpperCase() + idx.slice(1)}
</Body>
</span>
</div>
</div>
{/each}
</div>
</Layout>
<Layout noPadding gap="XS">
<Detail size="S">Actions</Detail>
<div class="item-list">
{#each Object.entries(internal) as [idx, action]}
{@const isDisabled = checkDisabled(idx) && checkDisabled(idx).disabled}
<div
class="item"
class:disabled={isDisabled}
class:selected={selectedAction === action.name}
on:click={isDisabled ? null : () => selectAction(action)}
>
<div class="item-body">
<Icon name={action.icon} />
<Body size="XS">{action.name}</Body>
{#if isDisabled && !syncAutomationsEnabled && !triggerAutomationRunEnabled && lockedFeatures.includes(action.stepId)}
<div class="tag-color">
<Tags>
<Tag icon="LockClosed">Premium</Tag>
</Tags>
</div>
{:else if isDisabled}
<Icon name="Help" tooltip={checkDisabled(idx).message} />
{:else if action.new}
<Tags>
<Tag emphasized>New</Tag>
</Tags>
{/if}
</div>
</div>
{/each}
</div>
</Layout>
{#if Object.keys(plugins).length}
<Layout noPadding gap="XS">
<Detail size="S">Plugins</Detail>
<div class="item-list">
{#each Object.entries(plugins) as [_, action]}
<div
class="item"
class:selected={selectedAction === action.name}
on:click={() => selectAction(action)}
>
<div class="item-body">
<Icon name={action.icon} />
<Body size="XS">{action.name}</Body>
</div>
</div>
{/each}
</div>
</Layout>
{/if}
</ModalContent>
<style>
.item-body {
display: flex;
margin-left: var(--spacing-m);
gap: var(--spacing-m);
align-items: center;
}
.item-list {
display: grid;
grid-template-columns: repeat(2, minmax(150px, 1fr));
grid-gap: var(--spectrum-alias-grid-baseline);
}
.item :global(.spectrum-Tags-itemLabel) {
cursor: pointer;
}
.item {
cursor: pointer;
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
padding: var(--spectrum-alias-item-padding-s);
background: var(--spectrum-alias-background-color-secondary);
transition: 0.3s all;
border: solid var(--spectrum-alias-border-color);
border-radius: 5px;
box-sizing: border-box;
border-width: 2px;
min-height: 3.5rem;
display: flex;
}
.item:not(.disabled):hover,
.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
.disabled {
background: var(--spectrum-global-color-gray-200);
color: var(--spectrum-global-color-gray-500);
}
.disabled :global(.spectrum-Body) {
color: var(--spectrum-global-color-gray-600);
}
.tag-color :global(.spectrum-Tags-item) {
background: var(--spectrum-global-color-gray-200);
}
</style>

View File

@ -241,7 +241,7 @@
align-items: center;
width: 100%;
background: var(--background);
padding: var(--spacing-m) var(--spacing-l);
padding: var(--spacing-m) var(--spacing-l) var(--spacing-s) 0;
box-sizing: border-box;
justify-content: space-between;
border-bottom: 1px solid var(--spectrum-global-color-gray-200);

View File

@ -1,18 +1,13 @@
<script>
import { Icon, TooltipPosition, TooltipType, Modal } from "@budibase/bbui"
import { Icon, TooltipPosition, TooltipType } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import ActionModal from "./ActionModal.svelte"
import { automationStore } from "@/stores/builder"
export let block
const dispatch = createEventDispatcher()
let actionModal
</script>
<Modal bind:this={actionModal} width="30%">
<ActionModal modal={actionModal} {block} />
</Modal>
<div class="action-bar">
{#if !block.branchNode}
<Icon
@ -31,7 +26,7 @@
hoverable
name="AddCircle"
on:click={() => {
actionModal.show()
automationStore.actions.openActionPanel(block)
}}
tooltipType={TooltipType.Info}
tooltipPosition={TooltipPosition.Right}

View File

@ -0,0 +1,411 @@
<script lang="ts">
import {
Detail,
Body,
Icon,
notifications,
Tags,
Tag,
Search,
} from "@budibase/bbui"
import Panel from "@/components/design/Panel.svelte"
import { AutomationActionStepId, BlockDefinitionTypes } from "@budibase/types"
import { automationStore, selectedAutomation } from "@/stores/builder"
import { admin, licensing } from "@/stores/portal"
import { externalActions } from "./ExternalActions"
import { TriggerStepID, ActionStepID } from "@/constants/backend/automations"
import type { AutomationStepDefinition } from "@budibase/types"
import { onMount } from "svelte"
import { fly } from "svelte/transition"
export let block
export let onClose = () => {}
let searchString: string = ""
let searchRef: HTMLInputElement | undefined = undefined
$: syncAutomationsEnabled = $licensing.syncAutomationsEnabled
$: triggerAutomationRunEnabled = $licensing.triggerAutomationRunEnabled
let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK]
let selectedAction: string | undefined
let actions = Object.entries($automationStore.blockDefinitions.ACTION).filter(
([key, action]) =>
key !== AutomationActionStepId.BRANCH && action.deprecated !== true
)
$: {
const triggerStepId = $selectedAutomation.data?.definition.trigger.stepId
if (triggerStepId && !collectBlockAllowedSteps.includes(triggerStepId)) {
actions = actions.filter(
([key]) => key !== AutomationActionStepId.COLLECT
)
}
}
let lockedFeatures = [
ActionStepID.COLLECT,
ActionStepID.TRIGGER_AUTOMATION_RUN,
]
$: blockRef = $selectedAutomation.blockRefs?.[block.id]
$: lastStep = blockRef?.terminating
$: pathSteps =
block.id && $selectedAutomation?.data
? automationStore.actions.getPathSteps(
blockRef?.pathTo,
$selectedAutomation.data
)
: []
$: collectBlockExists = pathSteps?.some(
step => step.stepId === ActionStepID.COLLECT
)
$: disabledStates = {
SEND_EMAIL_SMTP: {
disabled:
!$admin.checklist?.smtp?.checked || $admin.checklist?.smtp.fallback,
message: "Please configure SMTP",
},
COLLECT: {
disabled: !lastStep || !syncAutomationsEnabled || collectBlockExists,
message: collectDisabledMessage(),
},
TRIGGER_AUTOMATION_RUN: {
disabled: !triggerAutomationRunEnabled,
message: "Please upgrade to a paid plan",
},
}
$: checkDisabled = (idx: string) => {
return disabledStates[idx as keyof typeof disabledStates]
}
const collectDisabledMessage = () => {
if (collectBlockExists) {
return "Only one Collect step allowed"
}
if (!lastStep) {
return "Only available as the last step"
}
}
const allActions: Record<string, AutomationStepDefinition> = {}
actions.forEach(([k, v]) => {
if (!v.deprecated) {
allActions[k] = v
}
})
const plugins = actions.reduce(
(acc: Record<string, AutomationStepDefinition>, elm) => {
const [k, v] = elm
if (v.custom) {
acc[k] = v
}
return acc
},
{}
)
const categories = [
{
name: "Records",
items: actions.filter(([k]) =>
[
AutomationActionStepId.CREATE_ROW,
AutomationActionStepId.UPDATE_ROW,
AutomationActionStepId.DELETE_ROW,
AutomationActionStepId.QUERY_ROWS,
].includes(k as AutomationActionStepId)
),
},
{
name: "Flow logic",
items: actions.filter(([k]) =>
[
AutomationActionStepId.FILTER,
AutomationActionStepId.DELAY,
AutomationActionStepId.BRANCH,
AutomationActionStepId.TRIGGER_AUTOMATION_RUN,
AutomationActionStepId.COLLECT,
].includes(k as AutomationActionStepId)
),
},
{
name: "Code",
items: actions.filter(([k]) =>
[
AutomationActionStepId.EXECUTE_BASH,
AutomationActionStepId.EXECUTE_SCRIPT_V2,
AutomationActionStepId.SERVER_LOG,
].includes(k as AutomationActionStepId)
),
},
{
name: "Email",
items: actions.filter(([k]) =>
[AutomationActionStepId.SEND_EMAIL_SMTP].includes(
k as AutomationActionStepId
)
),
},
{
name: "Apps",
items: actions.filter(([k]) =>
[
AutomationActionStepId.zapier,
AutomationActionStepId.n8n,
AutomationActionStepId.integromat,
AutomationActionStepId.discord,
AutomationActionStepId.slack,
].includes(k as AutomationActionStepId)
),
},
]
$: filteredCategories = categories
.map(category => ({
...category,
items: category.items.filter(([_, action]) => {
const term = searchString.trim().toLowerCase()
if (!term) return true
const name = action.name?.toLowerCase() || ""
const stepTitle = action.stepTitle?.toLowerCase() || ""
return name.includes(term) || stepTitle.includes(term)
}),
}))
.filter(category => category.items.length > 0)
$: filteredPlugins = Object.entries(plugins).filter(([_, action]) => {
const term = searchString.trim().toLowerCase()
if (!term) return true
const name = action.name?.toLowerCase() || ""
const stepTitle = action.stepTitle?.toLowerCase() || ""
return name.includes(term) || stepTitle.includes(term)
})
const selectAction = async (action: AutomationStepDefinition) => {
selectedAction = action.name
try {
const newBlock = automationStore.actions.constructBlock(
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)
automationStore.actions.closeActionPanel()
onClose()
} catch (error) {
console.error(error)
notifications.error("Error saving automation")
}
}
const getExternalAction = (stepId: string) => {
return externalActions[stepId as keyof typeof externalActions]
}
onMount(() => {
searchRef?.focus()
})
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="container" transition:fly|local={{ x: 260, duration: 300 }}>
<Panel
title="Automation Step"
showCloseButton
onClickCloseButton={onClose}
customWidth={400}
borderLeft
>
<div class="step-panel-content">
<div class="search-container">
<Search
placeholder="Search"
value={searchString}
on:change={e => (searchString = e.detail)}
bind:inputRef={searchRef}
/>
</div>
{#each filteredCategories as category, i}
{#if i > 0}
<div class="section-divider" />
{/if}
<Detail size="M" weight={600}>{category.name}</Detail>
<div class="item-list">
{#each category.items as [idx, action]}
{@const isDisabled =
checkDisabled(idx) && checkDisabled(idx).disabled}
<div
class="item"
class:disabled={isDisabled}
class:selected={selectedAction === action.name}
on:click={isDisabled ? null : () => selectAction(action)}
>
<div class="item-body">
{#if !action.internal && getExternalAction(action.stepId)?.icon}
<img
width={24}
height={24}
src={getExternalAction(action.stepId)?.icon}
alt={getExternalAction(action.stepId)?.name}
class="external-icon"
/>
{:else}
<Icon name={action.icon} size="L" />
{/if}
<Body size="S" weight="400">
{action.internal === false
? action.stepTitle ||
idx.charAt(0).toUpperCase() + idx.slice(1)
: action.name}
</Body>
{#if isDisabled && !syncAutomationsEnabled && !triggerAutomationRunEnabled && lockedFeatures.includes(action.stepId)}
<div class="tag-color">
<Tags>
<Tag icon="LockClosed">Premium</Tag>
</Tags>
</div>
{:else if isDisabled}
<Icon name="Help" tooltip={checkDisabled(idx).message} />
{:else if action.new}
<Tags>
<Tag emphasized>New</Tag>
</Tags>
{/if}
</div>
</div>
{/each}
</div>
{/each}
{#if filteredPlugins.length}
<div class="section-divider" />
<div class="section-header">
<Detail size="M" weight={700}>Plugins</Detail>
</div>
<div class="item-list">
{#each filteredPlugins as [_, action]}
<div
class="item"
class:selected={selectedAction === action.name}
on:click={() => selectAction(action)}
>
<div class="item-body">
<div class="item-icon">
<Icon name={action.icon} size="L" />
</div>
<div class="item-label">
<Body size="M" weight="400">{action.name}</Body>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
</Panel>
</div>
<style>
.container {
position: fixed;
right: 0;
z-index: 1;
height: calc(100% - 60px);
display: flex;
flex-direction: row;
align-items: stretch;
}
.step-panel-content {
flex: 1 1 auto;
overflow-y: auto;
padding: 10px 15px 10px 15px;
display: flex;
flex-direction: column;
gap: 0;
}
.search-container {
margin-bottom: var(--spacing-xl);
}
.section-header,
.step-panel-content :global(.spectrum-Detail) {
margin-bottom: var(--spacing-s);
color: var(--spectrum-global-color-gray-700) !important;
}
.section-divider {
border-top: 1px solid var(--spectrum-global-color-gray-200);
height: 1px;
background: var(--spectrum-global-color-gray-200);
margin: 18px 0 10px 0;
width: 100%;
}
.item-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.item {
border-radius: 8px;
padding: 0 12px;
margin-bottom: 0px;
transition: box-shadow 0.2s, background 0.2s;
border: 0.5px solid var(--spectrum-alias-border-color);
background: var(--spectrum-alias-background-color-secondary);
display: flex;
align-items: center;
min-height: 40px;
height: 40px;
box-sizing: border-box;
cursor: pointer;
}
.item:not(.disabled):hover,
.selected {
background: var(--spectrum-alias-background-color-tertiary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.item.disabled {
opacity: 0.5;
filter: grayscale(0.5);
cursor: not-allowed;
}
.item-body {
display: flex;
align-items: center;
gap: var(--spacing-s);
width: 100%;
}
.item-icon,
.external-icon {
font-size: 24px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.item-label {
font-size: 15px;
font-weight: 400;
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--spectrum-global-color-gray-900) !important;
}
.tag-color :global(.spectrum-Tags-item) {
background: var(--spectrum-global-color-gray-200);
}
</style>

View File

@ -0,0 +1,15 @@
<script lang="ts">
</script>
<div class="new">NEW</div>
<style>
.new {
font-size: 8px;
background: var(--bb-indigo);
border-radius: 2px;
padding: 1px 3px;
color: white;
font-weight: bold;
}
</style>

View File

@ -1,26 +1,32 @@
<script>
<script lang="ts">
import { Icon, Body, TooltipPosition, TooltipType } from "@budibase/bbui"
export let title
export let icon
export let iconTooltip
export let showAddButton = false
export let showBackButton = false
export let showCloseButton = false
export let onClickAddButton
export let onClickBackButton
export let onClickCloseButton
export let borderLeft = false
export let borderRight = false
export let borderBottomHeader = true
export let wide = false
export let extraWide = false
export let closeButtonIcon = "Close"
export let noHeaderBorder = false
export let titleCSS = true
export let title: string | undefined = ""
export let icon: string | undefined = ""
export let iconTooltip: string | undefined = ""
export let showAddButton: boolean | undefined = false
export let showBackButton: boolean | undefined = false
export let showCloseButton: boolean | undefined = false
export let onClickAddButton: () => void = () => {}
export let onClickBackButton: () => void = () => {}
export let onClickCloseButton: () => void = () => {}
export let borderLeft: boolean | undefined = false
export let borderRight: boolean | undefined = false
export let borderBottomHeader: boolean | undefined = true
export let wide: boolean | undefined = false
export let extraWide: boolean | undefined = false
export let closeButtonIcon: string | undefined = "Close"
export let noHeaderBorder: boolean | undefined = false
export let titleCSS: boolean | undefined = true
export let customWidth: number | undefined = undefined
$: customHeaderContent = $$slots["panel-header-content"]
$: customTitleContent = $$slots["panel-title-content"]
$: panelStyle =
customWidth && !isNaN(customWidth)
? `min-width: ${customWidth}px; width: ${customWidth}px; flex: 0 0 ${customWidth}px;`
: undefined
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
@ -31,6 +37,7 @@
class:extraWide
class:borderLeft
class:borderRight
style={panelStyle}
>
<div
class="header"

View File

@ -12,6 +12,7 @@
selectedAutomation,
} from "@/stores/builder"
import StepPanel from "@/components/automation/AutomationBuilder/StepPanel.svelte"
import SelectStepSidePanel from "@/components/automation/AutomationBuilder/FlowChart/SelectStepSidePanel.svelte"
$: automationId = $selectedAutomation?.data?._id
$: blockRefs = $selectedAutomation.blockRefs
@ -68,6 +69,13 @@
</div>
{/if}
{#if $automationStore.actionPanelBlock && !$automationStore.selectedNodeId}
<SelectStepSidePanel
block={$automationStore.actionPanelBlock}
onClose={() => automationStore.actions.closeActionPanel()}
/>
{/if}
<Modal bind:this={modal}>
<CreateAutomationModal {webhookModal} />
</Modal>
@ -118,7 +126,7 @@
background-color: var(--background);
overflow: auto;
grid-column: 3;
width: 360px;
max-width: 360px;
width: 400px;
max-width: 400px;
}
</style>

View File

@ -12,6 +12,7 @@
import { onMount } from "svelte"
import { fly } from "svelte/transition"
import { findComponentPath } from "@/helpers/components"
import NewPill from "@/components/common/NewPill.svelte"
// Smallest possible 1x1 transparent GIF
const ghost = new Image(1, 1)
@ -272,7 +273,7 @@
<div class="component-name">
<Body size="XS">{component.name}</Body>
{#if component.new}
<div class="new">NEW</div>
<NewPill />
{/if}
</div>
</div>
@ -330,16 +331,6 @@
align-items: center;
gap: 4px;
}
.new {
font-size: 8px;
color: white;
background: var(--bb-indigo);
border-radius: 2px;
padding: 1px 3px;
font-weight: bold;
margin-left: auto;
flex-shrink: 0;
}
.component :global(.spectrum-Body) {
line-height: 1.2 !important;
overflow: hidden;

View File

@ -6,6 +6,7 @@
import StatePanel from "./StatePanel.svelte"
import BindingsPanel from "./BindingsPanel.svelte"
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
import NewPill from "@/components/common/NewPill.svelte"
const [resizable, resizableHandle] = getHorizontalResizeActions()
@ -31,7 +32,7 @@
<div class="tab-label">
{tab}
{#if tab !== Tabs.Components}
<div class="new">NEW</div>
<NewPill />
{/if}
</div>
</ActionButton>
@ -88,14 +89,6 @@
align-items: center;
gap: 4px;
}
.new {
font-size: 8px;
background: var(--bb-indigo);
border-radius: 2px;
padding: 1px 3px;
color: white;
font-weight: bold;
}
.divider {
position: relative;

View File

@ -2008,6 +2008,20 @@ const automationActions = (store: AutomationStore) => ({
}
})
},
openActionPanel: (block: BlockRef) => {
store.update(state => ({
...state,
actionPanelBlock: block,
selectedNodeId: undefined,
}))
},
closeActionPanel: () => {
store.update(state => ({
...state,
actionPanelBlock: undefined,
}))
},
})
export interface AutomationContext {

View File

@ -129,6 +129,7 @@ export interface AutomationState {
appSelf?: AppSelfResponse
selectedNodeId?: string
selectedNodeMode?: DataMode
actionPanelBlock?: BlockRef
}
export interface DerivedAutomationState extends AutomationState {

View File

@ -16,6 +16,7 @@ import {
UserCtx,
DeploymentStatus,
DeploymentProgressResponse,
Automation,
} from "@budibase/types"
import sdk from "../../../sdk"
import { builderSocket } from "../../../websockets"
@ -76,17 +77,24 @@ async function initDeployedApp(prodAppId: any) {
const db = context.getProdAppDB()
console.log("Reading automation docs")
const automations = (
await db.allDocs(
await db.allDocs<Automation>(
getAutomationParams(null, {
include_docs: true,
})
)
).rows.map((row: any) => row.doc)
).rows.map(row => row.doc!)
await clearMetadata()
const { count } = await disableAllCrons(prodAppId)
const promises = []
for (let automation of automations) {
promises.push(enableCronTrigger(prodAppId, automation))
promises.push(
enableCronTrigger(prodAppId, automation).catch(err => {
throw new Error(
`Failed to enable CRON trigger for automation "${automation.name}": ${err.message}`,
{ cause: err }
)
})
)
}
const results = await Promise.all(promises)
const enabledCount = results
@ -207,10 +215,8 @@ export const publishApp = async function (
} catch (err: any) {
deployment.setStatus(DeploymentStatus.FAILURE, err.message)
await storeDeploymentHistory(deployment)
throw {
...err,
message: `Deployment Failed: ${err.message}`,
}
throw new Error(`Deployment Failed: ${err.message}`, { cause: err })
} finally {
if (replication) {
await replication.close()

View File

@ -51,7 +51,7 @@ describe("cron trigger", () => {
})
it("should fail if the cron expression is invalid", async () => {
await createAutomationBuilder(config)
const { automation } = await createAutomationBuilder(config)
.onCron({ cron: "* * * * * *" })
.serverLog({
text: "Hello, world!",
@ -61,8 +61,7 @@ describe("cron trigger", () => {
await config.api.application.publish(config.getAppId(), {
status: 500,
body: {
message:
'Deployment Failed: Invalid automation CRON "* * * * * *" - Expected 5 values, but got 6.',
message: `Deployment Failed: Failed to enable CRON trigger for automation "${automation.name}": Invalid automation CRON "* * * * * *" - Expected 5 values, but got 6.`,
},
})
})

View File

@ -182,7 +182,7 @@ export async function enableCronTrigger(appId: any, automation: Automation) {
!automation.disabled
) {
const inputs = trigger.inputs as CronTriggerInputs
const cronExp = inputs.cron
const cronExp = inputs.cron || ""
const validation = helpers.cron.validate(cronExp)
if (!validation.valid) {
throw new Error(

View File

@ -14,7 +14,7 @@ export const definition: AutomationStepDefinition = {
description: "Run a piece of JavaScript code in your automation",
type: AutomationStepType.ACTION,
internal: true,
new: true,
new: false,
stepId: AutomationActionStepId.EXECUTE_SCRIPT_V2,
inputs: {},
features: {