diff --git a/packages/builder/src/pages/builder/portal/settings/ai/AIConfigTile.svelte b/packages/builder/src/pages/builder/portal/settings/ai/AIConfigTile.svelte index 2907919ce7..fb040204c2 100644 --- a/packages/builder/src/pages/builder/portal/settings/ai/AIConfigTile.svelte +++ b/packages/builder/src/pages/builder/portal/settings/ai/AIConfigTile.svelte @@ -4,6 +4,7 @@ import OpenAILogo from "./logos/OpenAI.svelte" import AnthropicLogo from "./logos/Anthropic.svelte" import TogetherAILogo from "./logos/TogetherAI.svelte" + import AzureOpenAILogo from "./logos/AzureOpenAI.svelte" import { Providers } from "./constants" const logos = { @@ -11,6 +12,7 @@ [Providers.OpenAI.name]: OpenAILogo, [Providers.Anthropic.name]: AnthropicLogo, [Providers.TogetherAI.name]: TogetherAILogo, + [Providers.AzureOpenAI.name]: AzureOpenAILogo, } export let config @@ -26,8 +28,8 @@
@@ -110,7 +112,7 @@ .tag { display: flex; - color: var(--spectrum-body-m-text-color); + color: #ffffff; padding: 4px 8px; justify-content: center; align-items: center; diff --git a/packages/builder/src/pages/builder/portal/settings/ai/AISettings.spec.js b/packages/builder/src/pages/builder/portal/settings/ai/AISettings.spec.js index fafead1ebf..300d5d5369 100644 --- a/packages/builder/src/pages/builder/portal/settings/ai/AISettings.spec.js +++ b/packages/builder/src/pages/builder/portal/settings/ai/AISettings.spec.js @@ -1,6 +1,6 @@ import { it, expect, describe, vi } from "vitest" 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 { notifications } from "@budibase/bbui" @@ -55,39 +55,43 @@ describe("AISettings", () => { expect(enterpriseTag).toBeInTheDocument() }) - it("should show the premium label on cloud when Budibase AI isn't enabled", 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 () => { + it("the add configuration button should not do anything the user doesn't have the correct license on cloud", async () => { let addConfigurationButton + let configModal setupEnv(Hosting.Cloud) instance = render(AISettings) 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 }) instance = render(AISettings) addConfigurationButton = instance.queryByText("Add configuration") 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 - - setupEnv(Hosting.Self) - instance = render(AISettings) - addConfigurationButton = instance.queryByText("Add configuration") - expect(addConfigurationButton).not.toBeInTheDocument() + let configModal setupEnv(Hosting.Self, { customAIConfigsEnabled: true }) - instance = render(AISettings, {}) + instance = render(AISettings) addConfigurationButton = instance.queryByText("Add configuration") expect(addConfigurationButton).toBeInTheDocument() + await fireEvent.click(addConfigurationButton) + configModal = instance.queryByText("Custom AI Configuration") + expect(configModal).toBeInTheDocument() }) }) }) diff --git a/packages/builder/src/pages/builder/portal/settings/ai/ConfigModal.svelte b/packages/builder/src/pages/builder/portal/settings/ai/ConfigModal.svelte index d480689028..26e7a1606c 100644 --- a/packages/builder/src/pages/builder/portal/settings/ai/ConfigModal.svelte +++ b/packages/builder/src/pages/builder/portal/settings/ai/ConfigModal.svelte @@ -84,8 +84,10 @@
- - +
+ + +
diff --git a/packages/builder/src/pages/builder/portal/settings/ai/logos/AzureOpenAI.svelte b/packages/builder/src/pages/builder/portal/settings/ai/logos/AzureOpenAI.svelte new file mode 100644 index 0000000000..647a32c9eb --- /dev/null +++ b/packages/builder/src/pages/builder/portal/settings/ai/logos/AzureOpenAI.svelte @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/builder/src/pages/builder/portal/settings/index.svelte b/packages/builder/src/pages/builder/portal/settings/index.svelte index 09ead3e410..1448b43ec4 100644 --- a/packages/builder/src/pages/builder/portal/settings/index.svelte +++ b/packages/builder/src/pages/builder/portal/settings/index.svelte @@ -1,5 +1,10 @@ diff --git a/packages/server/src/automations/tests/scenarios/looping.spec.ts b/packages/server/src/automations/tests/scenarios/looping.spec.ts index 6dde05ddb8..379927342a 100644 --- a/packages/server/src/automations/tests/scenarios/looping.spec.ts +++ b/packages/server/src/automations/tests/scenarios/looping.spec.ts @@ -5,6 +5,7 @@ import { LoopStepType, CreateRowStepOutputs, ServerLogStepOutputs, + FieldType, } from "@budibase/types" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" @@ -269,4 +270,145 @@ describe("Loop automations", () => { 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) + }) }) diff --git a/packages/server/src/automations/tests/scenarios/scenarios.spec.ts b/packages/server/src/automations/tests/scenarios/scenarios.spec.ts index 49c05984fe..850a04a95a 100644 --- a/packages/server/src/automations/tests/scenarios/scenarios.spec.ts +++ b/packages/server/src/automations/tests/scenarios/scenarios.spec.ts @@ -1,8 +1,9 @@ import * as automation from "../../index" import * as setup from "../utilities" -import { LoopStepType, FieldType } from "@budibase/types" +import { LoopStepType, FieldType, Table } from "@budibase/types" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" import { DatabaseName } from "../../../integrations/tests/utils" +import { FilterConditions } from "../../../automations/steps/filter" describe("Automation Scenarios", () => { 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", () => { @@ -233,4 +319,167 @@ describe("Automation Scenarios", () => { 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() + } + } + ) + }) }) diff --git a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts index 7528ea0f2e..2269f075b2 100644 --- a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts +++ b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts @@ -33,6 +33,7 @@ import { BranchStepInputs, SearchFilters, Branch, + FilterStepInputs, } from "@budibase/types" import TestConfiguration from "../../../tests/utilities/TestConfiguration" import * as setup from "../utilities" @@ -181,6 +182,14 @@ class BaseStepBuilder { opts?.stepName ) } + + filter(input: FilterStepInputs): this { + return this.step( + AutomationActionStepId.FILTER, + BUILTIN_ACTION_DEFINITIONS.FILTER, + input + ) + } } class StepBuilder extends BaseStepBuilder { build(): AutomationStep[] {