Merge branch 'master' into dean-fixes
This commit is contained in:
commit
b321eabb50
|
@ -1,4 +1,4 @@
|
|||
import { getComponentContexts } from "dataBinding"
|
||||
import { getAllComponentContexts } from "dataBinding"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
// Generates bindings for all components that provider "datasource like"
|
||||
|
@ -7,7 +7,7 @@ import { capitalise } from "helpers"
|
|||
// Some examples are saving rows or duplicating rows.
|
||||
export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
|
||||
// Get all form context providers
|
||||
const formComponentContexts = getComponentContexts(
|
||||
const formComponentContexts = getAllComponentContexts(
|
||||
asset,
|
||||
componentId,
|
||||
"form",
|
||||
|
@ -16,7 +16,7 @@ export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
|
|||
}
|
||||
)
|
||||
// Get all schema context providers
|
||||
const schemaComponentContexts = getComponentContexts(
|
||||
const schemaComponentContexts = getAllComponentContexts(
|
||||
asset,
|
||||
componentId,
|
||||
"schema",
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
findAllMatchingComponents,
|
||||
findComponent,
|
||||
findComponentPath,
|
||||
getComponentContexts,
|
||||
} from "helpers/components"
|
||||
import {
|
||||
componentStore,
|
||||
|
@ -213,7 +214,7 @@ export const getComponentBindableProperties = (asset, componentId) => {
|
|||
* both global and local bindings, taking into account a component's position
|
||||
* in the component tree.
|
||||
*/
|
||||
export const getComponentContexts = (
|
||||
export const getAllComponentContexts = (
|
||||
asset,
|
||||
componentId,
|
||||
type,
|
||||
|
@ -229,11 +230,6 @@ export const getComponentContexts = (
|
|||
|
||||
// Processes all contexts exposed by a component
|
||||
const processContexts = scope => component => {
|
||||
const def = componentStore.getDefinition(component._component)
|
||||
if (!def?.context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Filter out global contexts not in the same branch.
|
||||
// Global contexts are only valid if their branch root is an ancestor of
|
||||
// this component.
|
||||
|
@ -242,8 +238,8 @@ export const getComponentContexts = (
|
|||
return
|
||||
}
|
||||
|
||||
// Process all contexts provided by this component
|
||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||
const componentType = component._component
|
||||
const contexts = getComponentContexts(componentType)
|
||||
contexts.forEach(context => {
|
||||
// Ensure type matches
|
||||
if (type && context.type !== type) {
|
||||
|
@ -261,7 +257,7 @@ export const getComponentContexts = (
|
|||
if (!map[component._id]) {
|
||||
map[component._id] = {
|
||||
component,
|
||||
definition: def,
|
||||
definition: componentStore.getDefinition(componentType),
|
||||
contexts: [],
|
||||
}
|
||||
}
|
||||
|
@ -286,7 +282,7 @@ export const getComponentContexts = (
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets all data provider components above a component.
|
||||
* Gets all components available to this component that expose a certain action
|
||||
*/
|
||||
export const getActionProviders = (
|
||||
asset,
|
||||
|
@ -294,36 +290,30 @@ export const getActionProviders = (
|
|||
actionType,
|
||||
options = { includeSelf: false }
|
||||
) => {
|
||||
if (!asset) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get all components
|
||||
const components = findAllComponents(asset.props)
|
||||
|
||||
// Find matching contexts and generate bindings
|
||||
let providers = []
|
||||
components.forEach(component => {
|
||||
if (!options?.includeSelf && component._id === componentId) {
|
||||
return
|
||||
}
|
||||
const def = componentStore.getDefinition(component._component)
|
||||
const actions = (def?.actions || []).map(action => {
|
||||
return typeof action === "string" ? { type: action } : action
|
||||
})
|
||||
const action = actions.find(x => x.type === actionType)
|
||||
if (action) {
|
||||
let runtimeBinding = component._id
|
||||
if (action.suffix) {
|
||||
runtimeBinding += `-${action.suffix}`
|
||||
}
|
||||
providers.push({
|
||||
readableBinding: component._instanceName,
|
||||
runtimeBinding,
|
||||
})
|
||||
}
|
||||
const contexts = getAllComponentContexts(asset, componentId, "action", {
|
||||
includeSelf: options?.includeSelf,
|
||||
})
|
||||
return providers
|
||||
return (
|
||||
contexts
|
||||
// Find the definition of the action in question, if one is provided
|
||||
.map(context => ({
|
||||
...context,
|
||||
action: context.contexts[0]?.actions?.find(x => x.type === actionType),
|
||||
}))
|
||||
// Filter out contexts which don't have this action
|
||||
.filter(({ action }) => action != null)
|
||||
// Generate bindings for this component and action
|
||||
.map(({ component, action }) => {
|
||||
let runtimeBinding = component._id
|
||||
if (action.suffix) {
|
||||
runtimeBinding += `-${action.suffix}`
|
||||
}
|
||||
return {
|
||||
readableBinding: component._instanceName,
|
||||
runtimeBinding,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -371,7 +361,7 @@ export const getDatasourceForProvider = (asset, component) => {
|
|||
*/
|
||||
const getContextBindings = (asset, componentId) => {
|
||||
// Get all available contexts for this component
|
||||
const componentContexts = getComponentContexts(asset, componentId)
|
||||
const componentContexts = getAllComponentContexts(asset, componentId)
|
||||
|
||||
// Generate bindings for each context
|
||||
return componentContexts
|
||||
|
|
|
@ -228,6 +228,25 @@ export const getComponentName = component => {
|
|||
return componentDefinition.friendlyName || componentDefinition.name || ""
|
||||
}
|
||||
|
||||
// Gets all contexts exposed by a certain component type, including actions
|
||||
export const getComponentContexts = component => {
|
||||
const def = componentStore.getDefinition(component)
|
||||
let contexts = []
|
||||
if (def?.context) {
|
||||
contexts = Array.isArray(def.context) ? [...def.context] : [def.context]
|
||||
}
|
||||
if (def?.actions) {
|
||||
contexts.push({
|
||||
type: "action",
|
||||
scope: ContextScopes.Global,
|
||||
|
||||
// Ensure all actions are their verbose object versions
|
||||
actions: def.actions.map(x => (typeof x === "string" ? { type: x } : x)),
|
||||
})
|
||||
}
|
||||
return contexts
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurses through the component tree and builds a tree of contexts provided
|
||||
* by components.
|
||||
|
@ -243,10 +262,9 @@ export const buildContextTree = (
|
|||
}
|
||||
|
||||
// Process this component's contexts
|
||||
const def = componentStore.getDefinition(rootComponent._component)
|
||||
if (def?.context) {
|
||||
const contexts = getComponentContexts(rootComponent._component)
|
||||
if (contexts.length) {
|
||||
tree[currentBranch].push(rootComponent._id)
|
||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||
|
||||
// If we provide local context, start a new branch for our children
|
||||
if (contexts.some(context => context.scope === ContextScopes.Local)) {
|
||||
|
|
|
@ -147,6 +147,15 @@
|
|||
onOperatorChange(condition, condition.operator)
|
||||
}
|
||||
}
|
||||
|
||||
const onSettingChange = (e, condition) => {
|
||||
const setting = settings.find(x => x.key === e.detail)
|
||||
if (setting?.defaultValue != null) {
|
||||
condition.settingValue = setting.defaultValue
|
||||
} else {
|
||||
delete condition.settingValue
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
@ -189,7 +198,7 @@
|
|||
<Select
|
||||
options={settingOptions}
|
||||
bind:value={condition.setting}
|
||||
on:change={() => delete condition.settingValue}
|
||||
on:change={e => onSettingChange(e, condition)}
|
||||
/>
|
||||
<div>TO</div>
|
||||
{#if definition}
|
||||
|
|
|
@ -2,9 +2,16 @@
|
|||
import { Modal, ModalContent, ProgressBar } from "@budibase/bbui"
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { sleep } from "../../../utils/utils"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
const { clipboard, subscribe, copyAllowed, pasteAllowed, selectedCellCount } =
|
||||
getContext("grid")
|
||||
const {
|
||||
clipboard,
|
||||
subscribe,
|
||||
copyAllowed,
|
||||
pasteAllowed,
|
||||
selectedCellCount,
|
||||
focusedCellAPI,
|
||||
} = getContext("grid")
|
||||
const duration = 260
|
||||
|
||||
let modal
|
||||
|
@ -19,10 +26,15 @@
|
|||
}
|
||||
|
||||
const handlePasteRequest = async () => {
|
||||
// If a cell is active then let the native paste action take over
|
||||
if (get(focusedCellAPI)?.isActive()) {
|
||||
return
|
||||
}
|
||||
progressPercentage = 0
|
||||
if (!$pasteAllowed) {
|
||||
return
|
||||
}
|
||||
|
||||
// Prompt if paste will update multiple cells
|
||||
const multiCellPaste = $selectedCellCount > 1
|
||||
const prompt = $clipboard.multiCellCopy || multiCellPaste
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
case "c":
|
||||
return handle(() => dispatch("copy"))
|
||||
case "v":
|
||||
return handle(() => dispatch("paste"))
|
||||
return dispatch("paste")
|
||||
case "Enter":
|
||||
return handle(() => {
|
||||
if ($config.canAddRows) {
|
||||
|
|
|
@ -1,55 +1,36 @@
|
|||
import { Datasource, Query } from "@budibase/types"
|
||||
import * as setup from "./utilities"
|
||||
import {
|
||||
DatabaseName,
|
||||
getDatasource,
|
||||
knexClient,
|
||||
} from "../../integrations/tests/utils"
|
||||
import { DatabaseName } from "../../integrations/tests/utils"
|
||||
import { Knex } from "knex"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
|
||||
describe.each(
|
||||
[
|
||||
DatabaseName.POSTGRES,
|
||||
DatabaseName.MYSQL,
|
||||
DatabaseName.SQL_SERVER,
|
||||
DatabaseName.MARIADB,
|
||||
DatabaseName.ORACLE,
|
||||
].map(name => [name, getDatasource(name)])
|
||||
)("execute query action (%s)", (_, dsProvider) => {
|
||||
describe.each([
|
||||
DatabaseName.POSTGRES,
|
||||
DatabaseName.MYSQL,
|
||||
DatabaseName.SQL_SERVER,
|
||||
DatabaseName.MARIADB,
|
||||
DatabaseName.ORACLE,
|
||||
])("execute query action (%s)", name => {
|
||||
let tableName: string
|
||||
let client: Knex
|
||||
let datasource: Datasource
|
||||
let query: Query
|
||||
let config = setup.getConfig()
|
||||
const config = setup.getConfig()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
|
||||
const ds = await dsProvider
|
||||
datasource = await config.api.datasource.create(ds)
|
||||
client = await knexClient(ds)
|
||||
const testSetup = await setup.setupTestDatasource(config, name)
|
||||
datasource = testSetup.datasource
|
||||
client = testSetup.client
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
tableName = generator.guid()
|
||||
await client.schema.createTable(tableName, table => {
|
||||
table.string("a")
|
||||
table.integer("b")
|
||||
})
|
||||
await client(tableName).insert({ a: "string", b: 1 })
|
||||
query = await config.api.query.save({
|
||||
name: "test query",
|
||||
datasourceId: datasource._id!,
|
||||
parameters: [],
|
||||
fields: {
|
||||
sql: client(tableName).select("*").toSQL().toNative().sql,
|
||||
},
|
||||
transformer: "",
|
||||
schema: {},
|
||||
readable: true,
|
||||
queryVerb: "read",
|
||||
tableName = await setup.createTestTable(client, {
|
||||
a: { type: "string" },
|
||||
b: { type: "number" },
|
||||
})
|
||||
await setup.insertTestData(client, tableName, [{ a: "string", b: 1 }])
|
||||
query = await setup.saveTestQuery(config, client, tableName, datasource)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
import * as automation from "../../index"
|
||||
import * as setup from "../utilities"
|
||||
import { Table, LoopStepType } from "@budibase/types"
|
||||
import {
|
||||
Table,
|
||||
LoopStepType,
|
||||
CreateRowStepOutputs,
|
||||
ServerLogStepOutputs,
|
||||
FieldType,
|
||||
} from "@budibase/types"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationBuilder"
|
||||
import { DatabaseName } from "../../../integrations/tests/utils"
|
||||
|
||||
describe("Automation Scenarios", () => {
|
||||
let config = setup.getConfig(),
|
||||
|
@ -63,6 +70,72 @@ describe("Automation Scenarios", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should run an automation where a loop is successfully run twice", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Trigger with Loop and Create Row",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.rowSaved(
|
||||
{ tableId: table._id! },
|
||||
{
|
||||
row: {
|
||||
name: "Trigger Row",
|
||||
description: "This row triggers the automation",
|
||||
},
|
||||
id: "1234",
|
||||
revision: "1",
|
||||
}
|
||||
)
|
||||
.loop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: [1, 2, 3],
|
||||
})
|
||||
.createRow({
|
||||
row: {
|
||||
name: "Item {{ loop.currentItem }}",
|
||||
description: "Created from loop",
|
||||
tableId: table._id,
|
||||
},
|
||||
})
|
||||
.loop({
|
||||
option: LoopStepType.STRING,
|
||||
binding: "Message 1,Message 2,Message 3",
|
||||
})
|
||||
.serverLog({ text: "{{loop.currentItem}}" })
|
||||
.run()
|
||||
|
||||
expect(results.trigger).toBeDefined()
|
||||
expect(results.steps).toHaveLength(2)
|
||||
|
||||
expect(results.steps[0].outputs.iterations).toBe(3)
|
||||
expect(results.steps[0].outputs.items).toHaveLength(3)
|
||||
|
||||
results.steps[0].outputs.items.forEach(
|
||||
(output: CreateRowStepOutputs, index: number) => {
|
||||
expect(output).toMatchObject({
|
||||
success: true,
|
||||
row: {
|
||||
name: `Item ${index + 1}`,
|
||||
description: "Created from loop",
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
expect(results.steps[1].outputs.iterations).toBe(3)
|
||||
expect(results.steps[1].outputs.items).toHaveLength(3)
|
||||
|
||||
results.steps[1].outputs.items.forEach(
|
||||
(output: ServerLogStepOutputs, index: number) => {
|
||||
expect(output).toMatchObject({
|
||||
success: true,
|
||||
})
|
||||
expect(output.message).toContain(`Message ${index + 1}`)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Row Automations", () => {
|
||||
|
@ -157,4 +230,94 @@ describe("Automation Scenarios", () => {
|
|||
expect(results.steps[1].outputs.success).toBeTruthy()
|
||||
expect(results.steps[2].outputs.rows).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("should query an external database for some data then insert than into an internal table", async () => {
|
||||
const { datasource, client } = await setup.setupTestDatasource(
|
||||
config,
|
||||
DatabaseName.MYSQL
|
||||
)
|
||||
|
||||
const newTable = await config.createTable({
|
||||
name: "table",
|
||||
type: "table",
|
||||
schema: {
|
||||
name: {
|
||||
name: "name",
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
presence: true,
|
||||
},
|
||||
},
|
||||
age: {
|
||||
name: "age",
|
||||
type: FieldType.NUMBER,
|
||||
constraints: {
|
||||
presence: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const tableName = await setup.createTestTable(client, {
|
||||
name: { type: "string" },
|
||||
age: { type: "number" },
|
||||
})
|
||||
|
||||
const rows = [
|
||||
{ name: "Joe", age: 20 },
|
||||
{ name: "Bob", age: 25 },
|
||||
{ name: "Paul", age: 30 },
|
||||
]
|
||||
|
||||
await setup.insertTestData(client, tableName, rows)
|
||||
|
||||
const query = await setup.saveTestQuery(
|
||||
config,
|
||||
client,
|
||||
tableName,
|
||||
datasource
|
||||
)
|
||||
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test external query and save",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({
|
||||
fields: {},
|
||||
})
|
||||
.executeQuery({
|
||||
query: {
|
||||
queryId: query._id!,
|
||||
},
|
||||
})
|
||||
.loop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: "{{ steps.1.response }}",
|
||||
})
|
||||
.createRow({
|
||||
row: {
|
||||
name: "{{ loop.currentItem.name }}",
|
||||
age: "{{ loop.currentItem.age }}",
|
||||
tableId: newTable._id!,
|
||||
},
|
||||
})
|
||||
.queryRows({
|
||||
tableId: newTable._id!,
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(results.steps).toHaveLength(3)
|
||||
|
||||
expect(results.steps[1].outputs.iterations).toBe(3)
|
||||
expect(results.steps[1].outputs.items).toHaveLength(3)
|
||||
|
||||
expect(results.steps[2].outputs.rows).toHaveLength(3)
|
||||
|
||||
rows.forEach(expectedRow => {
|
||||
expect(results.steps[2].outputs.rows).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining(expectedRow)])
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
SmtpEmailStepInputs,
|
||||
ExecuteQueryStepInputs,
|
||||
QueryRowsStepInputs,
|
||||
ServerLogStepInputs,
|
||||
} from "@budibase/types"
|
||||
import {} from "../../steps/loop"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
|
@ -119,6 +120,10 @@ class AutomationBuilder {
|
|||
return this.step(BUILTIN_ACTION_DEFINITIONS.LOOP, inputs)
|
||||
}
|
||||
|
||||
serverLog(input: ServerLogStepInputs): this {
|
||||
return this.step(BUILTIN_ACTION_DEFINITIONS.SERVER_LOG, input)
|
||||
}
|
||||
|
||||
private trigger<T extends { [key: string]: any }>(
|
||||
triggerSchema: AutomationTriggerSchema,
|
||||
inputs?: T,
|
||||
|
|
|
@ -3,7 +3,14 @@ import { context } from "@budibase/backend-core"
|
|||
import { BUILTIN_ACTION_DEFINITIONS, getAction } from "../../actions"
|
||||
import emitter from "../../../events/index"
|
||||
import env from "../../../environment"
|
||||
import { AutomationActionStepId } from "@budibase/types"
|
||||
import { AutomationActionStepId, Datasource } from "@budibase/types"
|
||||
import { Knex } from "knex"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
import {
|
||||
getDatasource,
|
||||
knexClient,
|
||||
DatabaseName,
|
||||
} from "../../../integrations/tests/utils"
|
||||
|
||||
let config: TestConfig
|
||||
|
||||
|
@ -57,5 +64,58 @@ export async function runStep(stepId: string, inputs: any, stepContext?: any) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function createTestTable(client: Knex, schema: any) {
|
||||
const tableName = generator.guid()
|
||||
await client.schema.createTable(tableName, table => {
|
||||
for (const fieldName in schema) {
|
||||
const field = schema[fieldName]
|
||||
if (field.type === "string") {
|
||||
table.string(fieldName)
|
||||
} else if (field.type === "number") {
|
||||
table.integer(fieldName)
|
||||
}
|
||||
}
|
||||
})
|
||||
return tableName
|
||||
}
|
||||
|
||||
export async function insertTestData(
|
||||
client: Knex,
|
||||
tableName: string,
|
||||
rows: any[]
|
||||
) {
|
||||
await client(tableName).insert(rows)
|
||||
}
|
||||
|
||||
export async function saveTestQuery(
|
||||
config: TestConfig,
|
||||
client: Knex,
|
||||
tableName: string,
|
||||
datasource: Datasource
|
||||
) {
|
||||
return await config.api.query.save({
|
||||
name: "test query",
|
||||
datasourceId: datasource._id!,
|
||||
parameters: [],
|
||||
fields: {
|
||||
sql: client(tableName).select("*").toSQL().toNative().sql,
|
||||
},
|
||||
transformer: "",
|
||||
schema: {},
|
||||
readable: true,
|
||||
queryVerb: "read",
|
||||
})
|
||||
}
|
||||
|
||||
export async function setupTestDatasource(
|
||||
config: TestConfig,
|
||||
dbName: DatabaseName
|
||||
) {
|
||||
const db = await getDatasource(dbName)
|
||||
const datasource = await config.api.datasource.create(db)
|
||||
const client = await knexClient(db)
|
||||
return { datasource, client }
|
||||
}
|
||||
|
||||
export const apiKey = "test"
|
||||
export const actions = BUILTIN_ACTION_DEFINITIONS
|
||||
|
|
|
@ -98,6 +98,24 @@ export type ActionImplementations<T extends Hosting> = {
|
|||
}
|
||||
: {})
|
||||
|
||||
export type AutomationStepOutputs =
|
||||
| CollectStepOutputs
|
||||
| CreateRowStepOutputs
|
||||
| DelayStepOutputs
|
||||
| DeleteRowStepOutputs
|
||||
| ExecuteQueryStepOutputs
|
||||
| ExecuteScriptStepOutputs
|
||||
| FilterStepOutputs
|
||||
| QueryRowsStepOutputs
|
||||
| BaseAutomationOutputs
|
||||
| BashStepOutputs
|
||||
| ExternalAppStepOutputs
|
||||
| OpenAIStepOutputs
|
||||
| ServerLogStepOutputs
|
||||
| TriggerAutomationStepOutputs
|
||||
| UpdateRowStepOutputs
|
||||
| ZapierStepOutputs
|
||||
|
||||
export type BaseAutomationOutputs = {
|
||||
success?: boolean
|
||||
response?: {
|
||||
|
@ -199,7 +217,7 @@ export type LoopStepInputs = {
|
|||
}
|
||||
|
||||
export type LoopStepOutputs = {
|
||||
items: string
|
||||
items: AutomationStepOutputs[]
|
||||
success: boolean
|
||||
iterations: number
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue