Merge branch 'master' into refresh-all-datasources-peek
This commit is contained in:
commit
e1a1ba4936
|
@ -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": {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -129,6 +129,7 @@ export interface AutomationState {
|
|||
appSelf?: AppSelfResponse
|
||||
selectedNodeId?: string
|
||||
selectedNodeMode?: DataMode
|
||||
actionPanelBlock?: BlockRef
|
||||
}
|
||||
|
||||
export interface DerivedAutomationState extends AutomationState {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.`,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in New Issue