Merge remote-tracking branch 'origin/v3-ui' into feature/automation-branching-ux
This commit is contained in:
commit
8de5a0d7b6
|
@ -4,6 +4,7 @@
|
||||||
import OpenAILogo from "./logos/OpenAI.svelte"
|
import OpenAILogo from "./logos/OpenAI.svelte"
|
||||||
import AnthropicLogo from "./logos/Anthropic.svelte"
|
import AnthropicLogo from "./logos/Anthropic.svelte"
|
||||||
import TogetherAILogo from "./logos/TogetherAI.svelte"
|
import TogetherAILogo from "./logos/TogetherAI.svelte"
|
||||||
|
import AzureOpenAILogo from "./logos/AzureOpenAI.svelte"
|
||||||
import { Providers } from "./constants"
|
import { Providers } from "./constants"
|
||||||
|
|
||||||
const logos = {
|
const logos = {
|
||||||
|
@ -11,6 +12,7 @@
|
||||||
[Providers.OpenAI.name]: OpenAILogo,
|
[Providers.OpenAI.name]: OpenAILogo,
|
||||||
[Providers.Anthropic.name]: AnthropicLogo,
|
[Providers.Anthropic.name]: AnthropicLogo,
|
||||||
[Providers.TogetherAI.name]: TogetherAILogo,
|
[Providers.TogetherAI.name]: TogetherAILogo,
|
||||||
|
[Providers.AzureOpenAI.name]: AzureOpenAILogo,
|
||||||
}
|
}
|
||||||
|
|
||||||
export let config
|
export let config
|
||||||
|
@ -26,8 +28,8 @@
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={logos[config.name || config.provider]}
|
this={logos[config.name || config.provider]}
|
||||||
height="30"
|
height="18"
|
||||||
width="30"
|
width="18"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
@ -110,7 +112,7 @@
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
display: flex;
|
display: flex;
|
||||||
color: var(--spectrum-body-m-text-color);
|
color: #ffffff;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { it, expect, describe, vi } from "vitest"
|
import { it, expect, describe, vi } from "vitest"
|
||||||
import AISettings from "./index.svelte"
|
import AISettings from "./index.svelte"
|
||||||
import { render } from "@testing-library/svelte"
|
import { render, fireEvent } from "@testing-library/svelte"
|
||||||
import { admin, licensing } from "stores/portal"
|
import { admin, licensing } from "stores/portal"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
|
@ -55,39 +55,43 @@ describe("AISettings", () => {
|
||||||
expect(enterpriseTag).toBeInTheDocument()
|
expect(enterpriseTag).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should show the premium label on cloud when Budibase AI isn't enabled", async () => {
|
it("the add configuration button should not do anything the user doesn't have the correct license on cloud", async () => {
|
||||||
setupEnv(Hosting.Cloud)
|
|
||||||
instance = render(AISettings, {})
|
|
||||||
const premiumTag = instance.queryByText("Premium")
|
|
||||||
expect(premiumTag).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should not show the add configuration button if the user doesn't have the correct license on cloud", async () => {
|
|
||||||
let addConfigurationButton
|
let addConfigurationButton
|
||||||
|
let configModal
|
||||||
|
|
||||||
setupEnv(Hosting.Cloud)
|
setupEnv(Hosting.Cloud)
|
||||||
instance = render(AISettings)
|
instance = render(AISettings)
|
||||||
addConfigurationButton = instance.queryByText("Add configuration")
|
addConfigurationButton = instance.queryByText("Add configuration")
|
||||||
expect(addConfigurationButton).not.toBeInTheDocument()
|
expect(addConfigurationButton).toBeInTheDocument()
|
||||||
|
await fireEvent.click(addConfigurationButton)
|
||||||
|
configModal = instance.queryByText("Custom AI Configuration")
|
||||||
|
expect(configModal).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("the add configuration button should open the config modal if the user has the correct license on cloud", async () => {
|
||||||
|
let addConfigurationButton
|
||||||
|
let configModal
|
||||||
|
|
||||||
setupEnv(Hosting.Cloud, { customAIConfigsEnabled: true })
|
setupEnv(Hosting.Cloud, { customAIConfigsEnabled: true })
|
||||||
instance = render(AISettings)
|
instance = render(AISettings)
|
||||||
addConfigurationButton = instance.queryByText("Add configuration")
|
addConfigurationButton = instance.queryByText("Add configuration")
|
||||||
expect(addConfigurationButton).toBeInTheDocument()
|
expect(addConfigurationButton).toBeInTheDocument()
|
||||||
|
await fireEvent.click(addConfigurationButton)
|
||||||
|
configModal = instance.queryByText("Custom AI Configuration")
|
||||||
|
expect(configModal).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not show the add configuration button if the user doesn't have the correct license on self host", async () => {
|
it("the add configuration button should open the config modal if the user has the correct license on self host", async () => {
|
||||||
let addConfigurationButton
|
let addConfigurationButton
|
||||||
|
let configModal
|
||||||
setupEnv(Hosting.Self)
|
|
||||||
instance = render(AISettings)
|
|
||||||
addConfigurationButton = instance.queryByText("Add configuration")
|
|
||||||
expect(addConfigurationButton).not.toBeInTheDocument()
|
|
||||||
|
|
||||||
setupEnv(Hosting.Self, { customAIConfigsEnabled: true })
|
setupEnv(Hosting.Self, { customAIConfigsEnabled: true })
|
||||||
instance = render(AISettings, {})
|
instance = render(AISettings)
|
||||||
addConfigurationButton = instance.queryByText("Add configuration")
|
addConfigurationButton = instance.queryByText("Add configuration")
|
||||||
expect(addConfigurationButton).toBeInTheDocument()
|
expect(addConfigurationButton).toBeInTheDocument()
|
||||||
|
await fireEvent.click(addConfigurationButton)
|
||||||
|
configModal = instance.queryByText("Custom AI Configuration")
|
||||||
|
expect(configModal).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -84,8 +84,10 @@
|
||||||
<Label size="M">API Key</Label>
|
<Label size="M">API Key</Label>
|
||||||
<Input type="password" bind:value={config.apiKey} />
|
<Input type="password" bind:value={config.apiKey} />
|
||||||
</div>
|
</div>
|
||||||
<Toggle text="Active" bind:value={config.active} />
|
<div class="form-row">
|
||||||
<Toggle text="Set as default" bind:value={config.isDefault} />
|
<Toggle text="Active" bind:value={config.active} />
|
||||||
|
<Toggle text="Set as default" bind:value={config.isDefault} />
|
||||||
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const Providers = {
|
||||||
models: [{ label: "Llama 3 8B", value: "meta-llama/Meta-Llama-3-8B" }],
|
models: [{ label: "Llama 3 8B", value: "meta-llama/Meta-Llama-3-8B" }],
|
||||||
},
|
},
|
||||||
AzureOpenAI: {
|
AzureOpenAI: {
|
||||||
name: "Azure Open AI",
|
name: "Azure OpenAI",
|
||||||
models: [
|
models: [
|
||||||
{ label: "GPT 4o Mini", value: "gpt-4o-mini" },
|
{ label: "GPT 4o Mini", value: "gpt-4o-mini" },
|
||||||
{ label: "GPT 4o", value: "gpt-4o" },
|
{ label: "GPT 4o", value: "gpt-4o" },
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
let editingUuid
|
let editingUuid
|
||||||
|
|
||||||
$: isCloud = $admin.cloud
|
$: isCloud = $admin.cloud
|
||||||
$: budibaseAIEnabled = $licensing.budibaseAIEnabled
|
|
||||||
$: customAIConfigsEnabled = $licensing.customAIConfigsEnabled
|
$: customAIConfigsEnabled = $licensing.customAIConfigsEnabled
|
||||||
|
|
||||||
async function fetchAIConfig() {
|
async function fetchAIConfig() {
|
||||||
|
@ -127,18 +126,8 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="M">AI</Heading>
|
<div class="header">
|
||||||
{#if isCloud && !budibaseAIEnabled}
|
<Heading size="M">AI</Heading>
|
||||||
<Tags>
|
|
||||||
<Tag icon="LockClosed">Premium</Tag>
|
|
||||||
</Tags>
|
|
||||||
{/if}
|
|
||||||
<Body>Configure your AI settings within this section:</Body>
|
|
||||||
</Layout>
|
|
||||||
<Divider />
|
|
||||||
<Layout noPadding>
|
|
||||||
<div class="config-heading">
|
|
||||||
<Heading size="S">AI Configurations</Heading>
|
|
||||||
{#if !isCloud && !customAIConfigsEnabled}
|
{#if !isCloud && !customAIConfigsEnabled}
|
||||||
<Tags>
|
<Tags>
|
||||||
<Tag icon="LockClosed">Premium</Tag>
|
<Tag icon="LockClosed">Premium</Tag>
|
||||||
|
@ -147,24 +136,43 @@
|
||||||
<Tags>
|
<Tags>
|
||||||
<Tag icon="LockClosed">Enterprise</Tag>
|
<Tag icon="LockClosed">Enterprise</Tag>
|
||||||
</Tags>
|
</Tags>
|
||||||
{:else}
|
|
||||||
<Button size="S" cta on:click={newConfig}>Add configuration</Button>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Body size="S"
|
<Body>Configure your AI settings within this section:</Body>
|
||||||
>Use the following interface to select your preferred AI configuration.</Body
|
|
||||||
>
|
|
||||||
<Body size="S">Select your AI Model:</Body>
|
|
||||||
{#if fullAIConfig?.config}
|
|
||||||
{#each Object.keys(fullAIConfig.config) as key}
|
|
||||||
<AIConfigTile
|
|
||||||
config={fullAIConfig.config[key]}
|
|
||||||
editHandler={() => editConfig(key)}
|
|
||||||
deleteHandler={() => deleteConfig(key)}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<Divider />
|
||||||
|
<div style={`opacity: ${customAIConfigsEnabled ? 1 : 0.5}`}>
|
||||||
|
<Layout noPadding>
|
||||||
|
<div class="config-heading">
|
||||||
|
<Heading size="S">AI Configurations</Heading>
|
||||||
|
<Button
|
||||||
|
size="S"
|
||||||
|
cta={customAIConfigsEnabled}
|
||||||
|
secondary={!customAIConfigsEnabled}
|
||||||
|
on:click={customAIConfigsEnabled ? newConfig : null}
|
||||||
|
>
|
||||||
|
Add configuration
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Body size="S"
|
||||||
|
>Use the following interface to select your preferred AI configuration.</Body
|
||||||
|
>
|
||||||
|
{#if customAIConfigsEnabled}
|
||||||
|
<Body size="S">Select your AI Model:</Body>
|
||||||
|
{/if}
|
||||||
|
{#if fullAIConfig?.config}
|
||||||
|
{#each Object.keys(fullAIConfig.config) as key}
|
||||||
|
<AIConfigTile
|
||||||
|
config={fullAIConfig.config[key]}
|
||||||
|
editHandler={customAIConfigsEnabled ? () => editConfig(key) : null}
|
||||||
|
deleteHandler={customAIConfigsEnabled
|
||||||
|
? () => deleteConfig(key)
|
||||||
|
: null}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -172,5 +180,12 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-bottom: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
<script>
|
||||||
|
export let width
|
||||||
|
export let height
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" {width} {height} viewBox="0 0 96 96">
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="e399c19f-b68f-429d-b176-18c2117ff73c"
|
||||||
|
x1="-1032.172"
|
||||||
|
x2="-1059.213"
|
||||||
|
y1="145.312"
|
||||||
|
y2="65.426"
|
||||||
|
gradientTransform="matrix(1 0 0 -1 1075 158)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0" stop-color="#114a8b" />
|
||||||
|
<stop offset="1" stop-color="#0669bc" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="ac2a6fc2-ca48-4327-9a3c-d4dcc3256e15"
|
||||||
|
x1="-1023.725"
|
||||||
|
x2="-1029.98"
|
||||||
|
y1="108.083"
|
||||||
|
y2="105.968"
|
||||||
|
gradientTransform="matrix(1 0 0 -1 1075 158)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0" stop-opacity=".3" />
|
||||||
|
<stop offset=".071" stop-opacity=".2" />
|
||||||
|
<stop offset=".321" stop-opacity=".1" />
|
||||||
|
<stop offset=".623" stop-opacity=".05" />
|
||||||
|
<stop offset="1" stop-opacity="0" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="a7fee970-a784-4bb1-af8d-63d18e5f7db9"
|
||||||
|
x1="-1027.165"
|
||||||
|
x2="-997.482"
|
||||||
|
y1="147.642"
|
||||||
|
y2="68.561"
|
||||||
|
gradientTransform="matrix(1 0 0 -1 1075 158)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0" stop-color="#3ccbf4" />
|
||||||
|
<stop offset="1" stop-color="#2892df" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
fill="url(#e399c19f-b68f-429d-b176-18c2117ff73c)"
|
||||||
|
d="M33.338 6.544h26.038l-27.03 80.087a4.152 4.152 0 0 1-3.933 2.824H8.149a4.145 4.145 0 0 1-3.928-5.47L29.404 9.368a4.152 4.152 0 0 1 3.934-2.825z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#0078d4"
|
||||||
|
d="M71.175 60.261h-41.29a1.911 1.911 0 0 0-1.305 3.309l26.532 24.764a4.171 4.171 0 0 0 2.846 1.121h23.38z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="url(#ac2a6fc2-ca48-4327-9a3c-d4dcc3256e15)"
|
||||||
|
d="M33.338 6.544a4.118 4.118 0 0 0-3.943 2.879L4.252 83.917a4.14 4.14 0 0 0 3.908 5.538h20.787a4.443 4.443 0 0 0 3.41-2.9l5.014-14.777 17.91 16.705a4.237 4.237 0 0 0 2.666.972H81.24L71.024 60.261l-29.781.007L59.47 6.544z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="url(#a7fee970-a784-4bb1-af8d-63d18e5f7db9)"
|
||||||
|
d="M66.595 9.364a4.145 4.145 0 0 0-3.928-2.82H33.648a4.146 4.146 0 0 1 3.928 2.82l25.184 74.62a4.146 4.146 0 0 1-3.928 5.472h29.02a4.146 4.146 0 0 0 3.927-5.472z"
|
||||||
|
/>
|
||||||
|
</svg>
|
|
@ -1,5 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
|
import { licensing } from "stores/portal"
|
||||||
|
|
||||||
$redirect("./auth")
|
if ($licensing.customAIConfigsEnabled) {
|
||||||
|
$redirect("./ai")
|
||||||
|
} else {
|
||||||
|
$redirect("./auth")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
LoopStepType,
|
LoopStepType,
|
||||||
CreateRowStepOutputs,
|
CreateRowStepOutputs,
|
||||||
ServerLogStepOutputs,
|
ServerLogStepOutputs,
|
||||||
|
FieldType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||||
|
|
||||||
|
@ -269,4 +270,145 @@ describe("Loop automations", () => {
|
||||||
|
|
||||||
expect(results.steps[1].outputs.message).toContain("- 3")
|
expect(results.steps[1].outputs.message).toContain("- 3")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should run an automation with a loop and update row step", async () => {
|
||||||
|
const table = await config.createTable({
|
||||||
|
name: "TestTable",
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
name: "value",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const rows = [
|
||||||
|
{ name: "Row 1", value: 1, tableId: table._id },
|
||||||
|
{ name: "Row 2", value: 2, tableId: table._id },
|
||||||
|
{ name: "Row 3", value: 3, tableId: table._id },
|
||||||
|
]
|
||||||
|
|
||||||
|
await config.api.row.bulkImport(table._id!, { rows })
|
||||||
|
|
||||||
|
const builder = createAutomationBuilder({
|
||||||
|
name: "Test Loop and Update Row",
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = await builder
|
||||||
|
.appAction({ fields: {} })
|
||||||
|
.queryRows({
|
||||||
|
tableId: table._id!,
|
||||||
|
})
|
||||||
|
.loop({
|
||||||
|
option: LoopStepType.ARRAY,
|
||||||
|
binding: "{{ steps.1.rows }}",
|
||||||
|
})
|
||||||
|
.updateRow({
|
||||||
|
rowId: "{{ loop.currentItem._id }}",
|
||||||
|
row: {
|
||||||
|
name: "Updated {{ loop.currentItem.name }}",
|
||||||
|
value: "{{ loop.currentItem.value }}",
|
||||||
|
tableId: table._id,
|
||||||
|
},
|
||||||
|
meta: {},
|
||||||
|
})
|
||||||
|
.queryRows({
|
||||||
|
tableId: table._id!,
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
|
||||||
|
const expectedRows = [
|
||||||
|
{ name: "Updated Row 1", value: 1 },
|
||||||
|
{ name: "Updated Row 2", value: 2 },
|
||||||
|
{ name: "Updated Row 3", value: 3 },
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(results.steps[1].outputs.items).toEqual(
|
||||||
|
expect.arrayContaining(
|
||||||
|
expectedRows.map(row =>
|
||||||
|
expect.objectContaining({
|
||||||
|
success: true,
|
||||||
|
row: expect.objectContaining(row),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(results.steps[2].outputs.rows).toEqual(
|
||||||
|
expect.arrayContaining(
|
||||||
|
expectedRows.map(row => expect.objectContaining(row))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(results.steps[1].outputs.items).toHaveLength(expectedRows.length)
|
||||||
|
expect(results.steps[2].outputs.rows).toHaveLength(expectedRows.length)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should run an automation with a loop and delete row step", async () => {
|
||||||
|
const table = await config.createTable({
|
||||||
|
name: "TestTable",
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
name: "value",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const rows = [
|
||||||
|
{ name: "Row 1", value: 1, tableId: table._id },
|
||||||
|
{ name: "Row 2", value: 2, tableId: table._id },
|
||||||
|
{ name: "Row 3", value: 3, tableId: table._id },
|
||||||
|
]
|
||||||
|
|
||||||
|
await config.api.row.bulkImport(table._id!, { rows })
|
||||||
|
|
||||||
|
const builder = createAutomationBuilder({
|
||||||
|
name: "Test Loop and Delete Row",
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = await builder
|
||||||
|
.appAction({ fields: {} })
|
||||||
|
.queryRows({
|
||||||
|
tableId: table._id!,
|
||||||
|
})
|
||||||
|
.loop({
|
||||||
|
option: LoopStepType.ARRAY,
|
||||||
|
binding: "{{ steps.1.rows }}",
|
||||||
|
})
|
||||||
|
.deleteRow({
|
||||||
|
tableId: table._id!,
|
||||||
|
id: "{{ loop.currentItem._id }}",
|
||||||
|
})
|
||||||
|
.queryRows({
|
||||||
|
tableId: table._id!,
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
|
||||||
|
expect(results.steps).toHaveLength(3)
|
||||||
|
|
||||||
|
expect(results.steps[2].outputs.rows).toHaveLength(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import * as automation from "../../index"
|
import * as automation from "../../index"
|
||||||
import * as setup from "../utilities"
|
import * as setup from "../utilities"
|
||||||
import { LoopStepType, FieldType } from "@budibase/types"
|
import { LoopStepType, FieldType, Table } from "@budibase/types"
|
||||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||||
import { DatabaseName } from "../../../integrations/tests/utils"
|
import { DatabaseName } from "../../../integrations/tests/utils"
|
||||||
|
import { FilterConditions } from "../../../automations/steps/filter"
|
||||||
|
|
||||||
describe("Automation Scenarios", () => {
|
describe("Automation Scenarios", () => {
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
|
@ -195,6 +196,91 @@ describe("Automation Scenarios", () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should trigger an automation which creates and then updates a row", async () => {
|
||||||
|
const table = await config.createTable({
|
||||||
|
name: "TestTable",
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
name: "value",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const builder = createAutomationBuilder({
|
||||||
|
name: "Test Create and Update Row",
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = await builder
|
||||||
|
.appAction({ fields: {} })
|
||||||
|
.createRow(
|
||||||
|
{
|
||||||
|
row: {
|
||||||
|
name: "Initial Row",
|
||||||
|
value: 1,
|
||||||
|
tableId: table._id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ stepName: "CreateRowStep" }
|
||||||
|
)
|
||||||
|
.updateRow(
|
||||||
|
{
|
||||||
|
rowId: "{{ steps.CreateRowStep.row._id }}",
|
||||||
|
row: {
|
||||||
|
name: "Updated Row",
|
||||||
|
value: 2,
|
||||||
|
tableId: table._id,
|
||||||
|
},
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
{ stepName: "UpdateRowStep" }
|
||||||
|
)
|
||||||
|
.queryRows(
|
||||||
|
{
|
||||||
|
tableId: table._id!,
|
||||||
|
},
|
||||||
|
{ stepName: "QueryRowsStep" }
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
|
||||||
|
expect(results.steps).toHaveLength(3)
|
||||||
|
|
||||||
|
expect(results.steps[0].outputs).toMatchObject({
|
||||||
|
success: true,
|
||||||
|
row: {
|
||||||
|
name: "Initial Row",
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(results.steps[1].outputs).toMatchObject({
|
||||||
|
success: true,
|
||||||
|
row: {
|
||||||
|
name: "Updated Row",
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedRows = [{ name: "Updated Row", value: 2 }]
|
||||||
|
|
||||||
|
expect(results.steps[2].outputs.rows).toEqual(
|
||||||
|
expect.arrayContaining(
|
||||||
|
expectedRows.map(row => expect.objectContaining(row))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Name Based Automations", () => {
|
describe("Name Based Automations", () => {
|
||||||
|
@ -233,4 +319,167 @@ describe("Automation Scenarios", () => {
|
||||||
expect(results.steps[2].outputs.rows).toHaveLength(1)
|
expect(results.steps[2].outputs.rows).toHaveLength(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
describe("Automations with filter", () => {
|
||||||
|
let table: Table
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
table = await config.createTable({
|
||||||
|
name: "TestTable",
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
name: "value",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should stop an automation if the condition is not met", async () => {
|
||||||
|
const builder = createAutomationBuilder({
|
||||||
|
name: "Test Equal",
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = await builder
|
||||||
|
.appAction({ fields: {} })
|
||||||
|
.createRow({
|
||||||
|
row: {
|
||||||
|
name: "Equal Test",
|
||||||
|
value: 10,
|
||||||
|
tableId: table._id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.queryRows({
|
||||||
|
tableId: table._id!,
|
||||||
|
})
|
||||||
|
.filter({
|
||||||
|
field: "{{ steps.2.rows.0.value }}",
|
||||||
|
condition: FilterConditions.EQUAL,
|
||||||
|
value: 20,
|
||||||
|
})
|
||||||
|
.serverLog({ text: "Equal condition met" })
|
||||||
|
.run()
|
||||||
|
|
||||||
|
expect(results.steps[2].outputs.success).toBeTrue()
|
||||||
|
expect(results.steps[2].outputs.result).toBeFalse()
|
||||||
|
expect(results.steps[3]).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should continue the automation if the condition is met", async () => {
|
||||||
|
const builder = createAutomationBuilder({
|
||||||
|
name: "Test Not Equal",
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = await builder
|
||||||
|
.appAction({ fields: {} })
|
||||||
|
.createRow({
|
||||||
|
row: {
|
||||||
|
name: "Not Equal Test",
|
||||||
|
value: 10,
|
||||||
|
tableId: table._id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.queryRows({
|
||||||
|
tableId: table._id!,
|
||||||
|
})
|
||||||
|
.filter({
|
||||||
|
field: "{{ steps.2.rows.0.value }}",
|
||||||
|
condition: FilterConditions.NOT_EQUAL,
|
||||||
|
value: 20,
|
||||||
|
})
|
||||||
|
.serverLog({ text: "Not Equal condition met" })
|
||||||
|
.run()
|
||||||
|
|
||||||
|
expect(results.steps[2].outputs.success).toBeTrue()
|
||||||
|
expect(results.steps[2].outputs.result).toBeTrue()
|
||||||
|
expect(results.steps[3].outputs.success).toBeTrue()
|
||||||
|
})
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
condition: FilterConditions.EQUAL,
|
||||||
|
value: 10,
|
||||||
|
rowValue: 10,
|
||||||
|
expectPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: FilterConditions.NOT_EQUAL,
|
||||||
|
value: 10,
|
||||||
|
rowValue: 20,
|
||||||
|
expectPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: FilterConditions.GREATER_THAN,
|
||||||
|
value: 10,
|
||||||
|
rowValue: 15,
|
||||||
|
expectPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: FilterConditions.LESS_THAN,
|
||||||
|
value: 10,
|
||||||
|
rowValue: 5,
|
||||||
|
expectPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: FilterConditions.GREATER_THAN,
|
||||||
|
value: 10,
|
||||||
|
rowValue: 5,
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: FilterConditions.LESS_THAN,
|
||||||
|
value: 10,
|
||||||
|
rowValue: 15,
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
it.each(testCases)(
|
||||||
|
"should pass the filter when condition is $condition",
|
||||||
|
async ({ condition, value, rowValue, expectPass }) => {
|
||||||
|
const builder = createAutomationBuilder({
|
||||||
|
name: `Test ${condition}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = await builder
|
||||||
|
.appAction({ fields: {} })
|
||||||
|
.createRow({
|
||||||
|
row: {
|
||||||
|
name: `${condition} Test`,
|
||||||
|
value: rowValue,
|
||||||
|
tableId: table._id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.queryRows({
|
||||||
|
tableId: table._id!,
|
||||||
|
})
|
||||||
|
.filter({
|
||||||
|
field: "{{ steps.2.rows.0.value }}",
|
||||||
|
condition,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
.serverLog({
|
||||||
|
text: `${condition} condition ${expectPass ? "passed" : "failed"}`,
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
|
||||||
|
expect(results.steps[2].outputs.result).toBe(expectPass)
|
||||||
|
if (expectPass) {
|
||||||
|
expect(results.steps[3].outputs.success).toBeTrue()
|
||||||
|
} else {
|
||||||
|
expect(results.steps[3]).toBeUndefined()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -33,6 +33,7 @@ import {
|
||||||
BranchStepInputs,
|
BranchStepInputs,
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
Branch,
|
Branch,
|
||||||
|
FilterStepInputs,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||||
import * as setup from "../utilities"
|
import * as setup from "../utilities"
|
||||||
|
@ -181,6 +182,14 @@ class BaseStepBuilder {
|
||||||
opts?.stepName
|
opts?.stepName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filter(input: FilterStepInputs): this {
|
||||||
|
return this.step(
|
||||||
|
AutomationActionStepId.FILTER,
|
||||||
|
BUILTIN_ACTION_DEFINITIONS.FILTER,
|
||||||
|
input
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
class StepBuilder extends BaseStepBuilder {
|
class StepBuilder extends BaseStepBuilder {
|
||||||
build(): AutomationStep[] {
|
build(): AutomationStep[] {
|
||||||
|
|
Loading…
Reference in New Issue