Merge branch 'master' into migrate-js-to-ts-1

This commit is contained in:
Sam Rose 2025-03-11 15:02:14 +00:00 committed by GitHub
commit 36eda4f312
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
98 changed files with 1832 additions and 883 deletions

View File

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

View File

@ -18,7 +18,6 @@
"eslint-plugin-jest": "28.9.0", "eslint-plugin-jest": "28.9.0",
"eslint-plugin-local-rules": "3.0.2", "eslint-plugin-local-rules": "3.0.2",
"eslint-plugin-svelte": "2.46.1", "eslint-plugin-svelte": "2.46.1",
"svelte-preprocess": "^6.0.3",
"husky": "^8.0.3", "husky": "^8.0.3",
"kill-port": "^1.6.1", "kill-port": "^1.6.1",
"lerna": "7.4.2", "lerna": "7.4.2",
@ -29,7 +28,9 @@
"prettier-plugin-svelte": "^2.3.0", "prettier-plugin-svelte": "^2.3.0",
"proper-lockfile": "^4.1.2", "proper-lockfile": "^4.1.2",
"svelte": "4.2.19", "svelte": "4.2.19",
"svelte-check": "^4.1.5",
"svelte-eslint-parser": "0.43.0", "svelte-eslint-parser": "0.43.0",
"svelte-preprocess": "^6.0.3",
"typescript": "5.7.2", "typescript": "5.7.2",
"typescript-eslint": "8.17.0", "typescript-eslint": "8.17.0",
"yargs": "^17.7.2" "yargs": "^17.7.2"

View File

@ -222,9 +222,12 @@ export class DatabaseImpl implements Database {
} }
async getMultiple<T extends Document>( async getMultiple<T extends Document>(
ids: string[], ids?: string[],
opts?: { allowMissing?: boolean; excludeDocs?: boolean } opts?: { allowMissing?: boolean; excludeDocs?: boolean }
): Promise<T[]> { ): Promise<T[]> {
if (!ids || ids.length === 0) {
return []
}
// get unique // get unique
ids = [...new Set(ids)] ids = [...new Set(ids)]
const includeDocs = !opts?.excludeDocs const includeDocs = !opts?.excludeDocs
@ -249,7 +252,7 @@ export class DatabaseImpl implements Database {
if (!opts?.allowMissing && someMissing) { if (!opts?.allowMissing && someMissing) {
const missing = response.rows.filter(row => rowUnavailable(row)) const missing = response.rows.filter(row => rowUnavailable(row))
const missingIds = missing.map(row => row.key).join(", ") const missingIds = missing.map(row => row.key).join(", ")
throw new Error(`Unable to get documents: ${missingIds}`) throw new Error(`Unable to get bulk documents: ${missingIds}`)
} }
return rows.map(row => (includeDocs ? row.doc! : row.value)) return rows.map(row => (includeDocs ? row.doc! : row.value))
} }

View File

@ -52,13 +52,13 @@ export class DDInstrumentedDatabase implements Database {
} }
getMultiple<T extends Document>( getMultiple<T extends Document>(
ids: string[], ids?: string[],
opts?: { allowMissing?: boolean | undefined } | undefined opts?: { allowMissing?: boolean | undefined } | undefined
): Promise<T[]> { ): Promise<T[]> {
return tracer.trace("db.getMultiple", async span => { return tracer.trace("db.getMultiple", async span => {
span.addTags({ span.addTags({
db_name: this.name, db_name: this.name,
num_docs: ids.length, num_docs: ids?.length || 0,
allow_missing: opts?.allowMissing, allow_missing: opts?.allowMissing,
}) })
const docs = await this.db.getMultiple<T>(ids, opts) const docs = await this.db.getMultiple<T>(ids, opts)

View File

@ -26,8 +26,9 @@ import {
import { import {
getAccountHolderFromUsers, getAccountHolderFromUsers,
isAdmin, isAdmin,
isCreator, creatorsInList,
validateUniqueUser, validateUniqueUser,
isCreatorAsync,
} from "./utils" } from "./utils"
import { import {
getFirstPlatformUser, getFirstPlatformUser,
@ -261,8 +262,16 @@ export class UserDB {
} }
const change = dbUser ? 0 : 1 // no change if there is existing user const change = dbUser ? 0 : 1 // no change if there is existing user
const creatorsChange =
(await isCreator(dbUser)) !== (await isCreator(user)) ? 1 : 0 let creatorsChange = 0
if (dbUser) {
const [isDbUserCreator, isUserCreator] = await creatorsInList([
dbUser,
user,
])
creatorsChange = isDbUserCreator !== isUserCreator ? 1 : 0
}
return UserDB.quotas.addUsers(change, creatorsChange, async () => { return UserDB.quotas.addUsers(change, creatorsChange, async () => {
if (!opts.isAccountHolder) { if (!opts.isAccountHolder) {
await validateUniqueUser(email, tenantId) await validateUniqueUser(email, tenantId)
@ -353,7 +362,7 @@ export class UserDB {
} }
newUser.userGroups = groups || [] newUser.userGroups = groups || []
newUsers.push(newUser) newUsers.push(newUser)
if (await isCreator(newUser)) { if (await isCreatorAsync(newUser)) {
newCreators.push(newUser) newCreators.push(newUser)
} }
} }
@ -453,10 +462,8 @@ export class UserDB {
})) }))
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete) const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
const creatorsEval = await Promise.all(usersToDelete.map(isCreator)) const creatorsEval = await creatorsInList(usersToDelete)
const creatorsToDeleteCount = creatorsEval.filter( const creatorsToDeleteCount = creatorsEval.filter(creator => creator).length
creator => !!creator
).length
const ssoUsersToDelete: AnyDocument[] = [] const ssoUsersToDelete: AnyDocument[] = []
for (let user of usersToDelete) { for (let user of usersToDelete) {
@ -533,7 +540,7 @@ export class UserDB {
await db.remove(userId, dbUser._rev!) await db.remove(userId, dbUser._rev!)
const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0 const creatorsToDelete = (await isCreatorAsync(dbUser)) ? 1 : 0
await UserDB.quotas.removeUsers(1, creatorsToDelete) await UserDB.quotas.removeUsers(1, creatorsToDelete)
await eventHelpers.handleDeleteEvents(dbUser) await eventHelpers.handleDeleteEvents(dbUser)
await cache.user.invalidateUser(userId) await cache.user.invalidateUser(userId)

View File

@ -2,39 +2,39 @@ import { User, UserGroup } from "@budibase/types"
import { generator, structures } from "../../../tests" import { generator, structures } from "../../../tests"
import { DBTestConfiguration } from "../../../tests/extra" import { DBTestConfiguration } from "../../../tests/extra"
import { getGlobalDB } from "../../context" import { getGlobalDB } from "../../context"
import { isCreator } from "../utils" import { isCreatorSync, creatorsInList } from "../utils"
const config = new DBTestConfiguration() const config = new DBTestConfiguration()
describe("Users", () => { describe("Users", () => {
it("User is a creator if it is configured as a global builder", async () => { it("User is a creator if it is configured as a global builder", () => {
const user: User = structures.users.user({ builder: { global: true } }) const user: User = structures.users.user({ builder: { global: true } })
expect(await isCreator(user)).toBe(true) expect(isCreatorSync(user, [])).toBe(true)
}) })
it("User is a creator if it is configured as a global admin", async () => { it("User is a creator if it is configured as a global admin", () => {
const user: User = structures.users.user({ admin: { global: true } }) const user: User = structures.users.user({ admin: { global: true } })
expect(await isCreator(user)).toBe(true) expect(isCreatorSync(user, [])).toBe(true)
}) })
it("User is a creator if it is configured with creator permission", async () => { it("User is a creator if it is configured with creator permission", () => {
const user: User = structures.users.user({ builder: { creator: true } }) const user: User = structures.users.user({ builder: { creator: true } })
expect(await isCreator(user)).toBe(true) expect(isCreatorSync(user, [])).toBe(true)
}) })
it("User is a creator if it is a builder in some application", async () => { it("User is a creator if it is a builder in some application", () => {
const user: User = structures.users.user({ builder: { apps: ["app1"] } }) const user: User = structures.users.user({ builder: { apps: ["app1"] } })
expect(await isCreator(user)).toBe(true) expect(isCreatorSync(user, [])).toBe(true)
}) })
it("User is a creator if it has CREATOR permission in some application", async () => { it("User is a creator if it has CREATOR permission in some application", () => {
const user: User = structures.users.user({ roles: { app1: "CREATOR" } }) const user: User = structures.users.user({ roles: { app1: "CREATOR" } })
expect(await isCreator(user)).toBe(true) expect(isCreatorSync(user, [])).toBe(true)
}) })
it("User is a creator if it has ADMIN permission in some application", async () => { it("User is a creator if it has ADMIN permission in some application", () => {
const user: User = structures.users.user({ roles: { app1: "ADMIN" } }) const user: User = structures.users.user({ roles: { app1: "ADMIN" } })
expect(await isCreator(user)).toBe(true) expect(isCreatorSync(user, [])).toBe(true)
}) })
it("User is a creator if it remains to a group with ADMIN permissions", async () => { it("User is a creator if it remains to a group with ADMIN permissions", async () => {
@ -59,7 +59,7 @@ describe("Users", () => {
await db.put(group) await db.put(group)
for (let user of users) { for (let user of users) {
await db.put(user) await db.put(user)
const creator = await isCreator(user) const creator = (await creatorsInList([user]))[0]
expect(creator).toBe(true) expect(creator).toBe(true)
} }
}) })

View File

@ -22,7 +22,7 @@ import {
} from "@budibase/types" } from "@budibase/types"
import * as context from "../context" import * as context from "../context"
import { getGlobalDB } from "../context" import { getGlobalDB } from "../context"
import { isCreator } from "./utils" import { creatorsInList } from "./utils"
import { UserDB } from "./db" import { UserDB } from "./db"
import { dataFilters } from "@budibase/shared-core" import { dataFilters } from "@budibase/shared-core"
@ -305,8 +305,8 @@ export async function getCreatorCount() {
let creators = 0 let creators = 0
async function iterate(startPage?: string) { async function iterate(startPage?: string) {
const page = await paginatedUsers({ bookmark: startPage }) const page = await paginatedUsers({ bookmark: startPage })
const creatorsEval = await Promise.all(page.data.map(isCreator)) const creatorsEval = await creatorsInList(page.data)
creators += creatorsEval.filter(creator => !!creator).length creators += creatorsEval.filter(creator => creator).length
if (page.hasNextPage) { if (page.hasNextPage) {
await iterate(page.nextPage) await iterate(page.nextPage)
} }

View File

@ -16,30 +16,47 @@ export const hasAdminPermissions = sdk.users.hasAdminPermissions
export const hasBuilderPermissions = sdk.users.hasBuilderPermissions export const hasBuilderPermissions = sdk.users.hasBuilderPermissions
export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions
export async function isCreator(user?: User | ContextUser) { export async function creatorsInList(
users: (User | ContextUser)[],
groups?: UserGroup[]
) {
const groupIds = [
...new Set(
users.filter(user => user.userGroups).flatMap(user => user.userGroups!)
),
]
const db = context.getGlobalDB()
groups = await db.getMultiple<UserGroup>(groupIds, { allowMissing: true })
return users.map(user => isCreatorSync(user, groups))
}
// fetches groups if no provided, but is async and shouldn't be looped with
export async function isCreatorAsync(user: User | ContextUser) {
let groups: UserGroup[] = []
if (user.userGroups) {
const db = context.getGlobalDB()
groups = await db.getMultiple<UserGroup>(user.userGroups)
}
return isCreatorSync(user, groups)
}
export function isCreatorSync(user: User | ContextUser, groups?: UserGroup[]) {
const isCreatorByUserDefinition = sdk.users.isCreator(user) const isCreatorByUserDefinition = sdk.users.isCreator(user)
if (!isCreatorByUserDefinition && user) { if (!isCreatorByUserDefinition && user) {
return await isCreatorByGroupMembership(user) return isCreatorByGroupMembership(user, groups)
} }
return isCreatorByUserDefinition return isCreatorByUserDefinition
} }
async function isCreatorByGroupMembership(user?: User | ContextUser) { function isCreatorByGroupMembership(
const userGroups = user?.userGroups || [] user: User | ContextUser,
if (userGroups.length > 0) { groups?: UserGroup[]
const db = context.getGlobalDB() ) {
const groups: UserGroup[] = [] const userGroups = groups?.filter(
for (let groupId of userGroups) { group => user.userGroups?.indexOf(group._id!) !== -1
try { )
const group = await db.get<UserGroup>(groupId) if (userGroups && userGroups.length > 0) {
groups.push(group) return userGroups.some(group =>
} catch (e: any) {
if (e.error !== "not_found") {
throw e
}
}
}
return groups.some(group =>
Object.values(group.roles || {}).includes(BUILTIN_ROLE_IDS.ADMIN) Object.values(group.roles || {}).includes(BUILTIN_ROLE_IDS.ADMIN)
) )
} }

View File

@ -1,52 +0,0 @@
import { range } from "lodash/fp"
import { structures } from "../.."
jest.mock("../../../src/context")
jest.mock("../../../src/db")
import * as context from "../../../src/context"
import * as db from "../../../src/db"
import { getCreatorCount } from "../../../src/users/users"
describe("Users", () => {
let getGlobalDBMock: jest.SpyInstance
let paginationMock: jest.SpyInstance
beforeEach(() => {
jest.resetAllMocks()
getGlobalDBMock = jest.spyOn(context, "getGlobalDB")
paginationMock = jest.spyOn(db, "pagination")
jest.spyOn(db, "getGlobalUserParams")
})
it("retrieves the number of creators", async () => {
const getUsers = (offset: number, limit: number, creators = false) => {
const opts = creators ? { builder: { global: true } } : undefined
return range(offset, limit).map(() => structures.users.user(opts))
}
const page1Data = getUsers(0, 8)
const page2Data = getUsers(8, 12, true)
getGlobalDBMock.mockImplementation(() => ({
name: "fake-db",
allDocs: () => ({
rows: [...page1Data, ...page2Data],
}),
}))
paginationMock.mockImplementationOnce(() => ({
data: page1Data,
hasNextPage: true,
nextPage: "1",
}))
paginationMock.mockImplementation(() => ({
data: page2Data,
hasNextPage: false,
nextPage: undefined,
}))
const creatorsCount = await getCreatorCount()
expect(creatorsCount).toBe(4)
expect(paginationMock).toHaveBeenCalledTimes(2)
})
})

View File

@ -1,14 +1,14 @@
<script> <script lang="ts">
import "@spectrum-css/textfield/dist/index-vars.css" import "@spectrum-css/textfield/dist/index-vars.css"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let value = "" export let value = ""
export let placeholder = null export let placeholder: string | undefined = undefined
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let id = null export let id: string | undefined = undefined
export let height = null export let height: string | number | undefined = undefined
export let minHeight = null export let minHeight: string | number | undefined = undefined
export const getCaretPosition = () => ({ export const getCaretPosition = () => ({
start: textarea.selectionStart, start: textarea.selectionStart,
end: textarea.selectionEnd, end: textarea.selectionEnd,
@ -16,18 +16,21 @@
export let align = null export let align = null
let focus = false let focus = false
let textarea let textarea: any
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = event => { const onChange = (event: any) => {
dispatch("change", event.target.value) dispatch("change", event.target.value)
focus = false focus = false
} }
const getStyleString = (attribute, value) => { const getStyleString = (
attribute: string,
value: string | number | undefined
) => {
if (!attribute || value == null) { if (!attribute || value == null) {
return "" return ""
} }
if (isNaN(value)) { if (typeof value === "number" && isNaN(value)) {
return `${attribute}:${value};` return `${attribute}:${value};`
} }
return `${attribute}:${value}px;` return `${attribute}:${value}px;`

View File

@ -1,21 +1,21 @@
<script> <script lang="ts">
import Field from "./Field.svelte" import Field from "./Field.svelte"
import TextArea from "./Core/TextArea.svelte" import TextArea from "./Core/TextArea.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let value = null export let value: string | undefined = undefined
export let label = null export let label: string | undefined = undefined
export let labelPosition = "above" export let labelPosition: string = "above"
export let placeholder = null export let placeholder: string | undefined = undefined
export let disabled = false export let disabled = false
export let error = null export let error: string | undefined = undefined
export let getCaretPosition = null export let getCaretPosition: any = undefined
export let height = null export let height: string | number | undefined = undefined
export let minHeight = null export let minHeight: string | number | undefined = undefined
export let helpText = null export let helpText: string | undefined = undefined
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = (e: any) => {
value = e.detail value = e.detail
dispatch("change", e.detail) dispatch("change", e.detail)
} }
@ -24,7 +24,6 @@
<Field {helpText} {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<TextArea <TextArea
bind:getCaretPosition bind:getCaretPosition
{error}
{disabled} {disabled}
{value} {value}
{placeholder} {placeholder}

View File

@ -1,6 +1,7 @@
<script> <script>
import "@spectrum-css/inlinealert/dist/index-vars.css" import "@spectrum-css/inlinealert/dist/index-vars.css"
import Button from "../Button/Button.svelte" import Button from "../Button/Button.svelte"
import Icon from "../Icon/Icon.svelte"
export let type = "info" export let type = "info"
export let header = "" export let header = ""
@ -8,6 +9,8 @@
export let onConfirm = undefined export let onConfirm = undefined
export let buttonText = "" export let buttonText = ""
export let cta = false export let cta = false
export let link = ""
export let linkText = ""
$: icon = selectIcon(type) $: icon = selectIcon(type)
// if newlines used, convert them to different elements // if newlines used, convert them to different elements
@ -49,6 +52,19 @@
> >
</div> </div>
{/if} {/if}
{#if link && linkText}
<div id="docs-link">
<a
href={link}
target="_blank"
rel="noopener noreferrer"
class="docs-link"
>
{linkText}
<Icon name="LinkOut" size="XS" />
</a>
</div>
{/if}
</div> </div>
<style> <style>
@ -64,4 +80,21 @@
margin: 0; margin: 0;
border-width: 1px; border-width: 1px;
} }
a {
color: white;
}
#docs-link {
padding-top: 10px;
display: flex;
align-items: center;
gap: 5px;
}
#docs-link > * {
display: flex;
align-items: center;
gap: 5px;
}
</style> </style>

View File

@ -4,7 +4,7 @@
export let title export let title
export let icon = "" export let icon = ""
export let id export let id = undefined
export let href = "#" export let href = "#"
export let link = false export let link = false

View File

@ -8,11 +8,13 @@
export let invalid: boolean = false export let invalid: boolean = false
export let disabled: boolean = false export let disabled: boolean = false
export let closable: boolean = false export let closable: boolean = false
export let emphasized: boolean = false
</script> </script>
<div <div
class:is-invalid={invalid} class:is-invalid={invalid}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-emphasized={emphasized}
class="spectrum-Tags-item" class="spectrum-Tags-item"
role="listitem" role="listitem"
> >
@ -40,4 +42,9 @@
margin-bottom: 0; margin-bottom: 0;
margin-top: 0; margin-top: 0;
} }
.is-emphasized {
border-color: var(--spectrum-global-color-blue-700);
color: var(--spectrum-global-color-blue-700);
}
</style> </style>

View File

@ -100,7 +100,6 @@
"jest": "29.7.0", "jest": "29.7.0",
"jsdom": "^21.1.1", "jsdom": "^21.1.1",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"svelte-check": "^4.1.0",
"svelte-jester": "^1.3.2", "svelte-jester": "^1.3.2",
"vite": "^4.5.0", "vite": "^4.5.0",
"vite-plugin-static-copy": "^0.17.0", "vite-plugin-static-copy": "^0.17.0",

View File

@ -23,9 +23,8 @@
let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK] let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK]
let selectedAction let selectedAction
let actions = Object.entries($automationStore.blockDefinitions.ACTION).filter( let actions = Object.entries($automationStore.blockDefinitions.ACTION).filter(
entry => { ([key, action]) => {
const [key] = entry return key !== AutomationActionStepId.BRANCH && action.deprecated !== true
return key !== AutomationActionStepId.BRANCH
} }
) )
let lockedFeatures = [ let lockedFeatures = [
@ -186,6 +185,10 @@
</div> </div>
{:else if isDisabled} {:else if isDisabled}
<Icon name="Help" tooltip={disabled()[idx].message} /> <Icon name="Help" tooltip={disabled()[idx].message} />
{:else if action.new}
<Tags>
<Tag emphasized>New</Tag>
</Tags>
{/if} {/if}
</div> </div>
</div> </div>
@ -227,6 +230,10 @@
grid-gap: var(--spectrum-alias-grid-baseline); grid-gap: var(--spectrum-alias-grid-baseline);
} }
.item :global(.spectrum-Tags-itemLabel) {
cursor: pointer;
}
.item { .item {
cursor: pointer; cursor: pointer;
grid-gap: var(--spectrum-alias-grid-margin-xsmall); grid-gap: var(--spectrum-alias-grid-margin-xsmall);
@ -237,6 +244,8 @@
border-radius: 5px; border-radius: 5px;
box-sizing: border-box; box-sizing: border-box;
border-width: 2px; border-width: 2px;
min-height: 3.5rem;
display: flex;
} }
.item:not(.disabled):hover, .item:not(.disabled):hover,
.selected { .selected {

View File

@ -18,8 +18,12 @@
import AutomationBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte" import AutomationBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte"
import FlowItemHeader from "./FlowItemHeader.svelte" import FlowItemHeader from "./FlowItemHeader.svelte"
import FlowItemActions from "./FlowItemActions.svelte" import FlowItemActions from "./FlowItemActions.svelte"
import { automationStore, selectedAutomation } from "@/stores/builder" import {
import { QueryUtils, Utils } from "@budibase/frontend-core" automationStore,
selectedAutomation,
evaluationContext,
} from "@/stores/builder"
import { QueryUtils, Utils, memo } from "@budibase/frontend-core"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { createEventDispatcher, getContext } from "svelte" import { createEventDispatcher, getContext } from "svelte"
import DragZone from "./DragZone.svelte" import DragZone from "./DragZone.svelte"
@ -34,11 +38,14 @@
export let automation export let automation
const view = getContext("draggableView") const view = getContext("draggableView")
const memoContext = memo({})
let drawer let drawer
let open = true let open = true
let confirmDeleteModal let confirmDeleteModal
$: memoContext.set($evaluationContext)
$: branch = step.inputs?.branches?.[branchIdx] $: branch = step.inputs?.branches?.[branchIdx]
$: editableConditionUI = branch.conditionUI || {} $: editableConditionUI = branch.conditionUI || {}
@ -100,6 +107,7 @@
allowOnEmpty={false} allowOnEmpty={false}
builderType={"condition"} builderType={"condition"}
docsURL={null} docsURL={null}
evaluationContext={$memoContext}
/> />
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>

View File

@ -1,6 +1,13 @@
<script> <script>
import { automationStore, selectedAutomation } from "@/stores/builder" import { automationStore, selectedAutomation } from "@/stores/builder"
import { Icon, Body, AbsTooltip, StatusLight } from "@budibase/bbui" import {
Icon,
Body,
AbsTooltip,
StatusLight,
Tags,
Tag,
} from "@budibase/bbui"
import { externalActions } from "./ExternalActions" import { externalActions } from "./ExternalActions"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { Features } from "@/constants/backend/automations" import { Features } from "@/constants/backend/automations"
@ -24,6 +31,7 @@
$: blockRefs = $selectedAutomation?.blockRefs || {} $: blockRefs = $selectedAutomation?.blockRefs || {}
$: stepNames = automation?.definition.stepNames || {} $: stepNames = automation?.definition.stepNames || {}
$: allSteps = automation?.definition.steps || [] $: allSteps = automation?.definition.steps || []
$: blockDefinition = $automationStore.blockDefinitions.ACTION[block.stepId]
$: automationName = itemName || stepNames?.[block.id] || block?.name || "" $: automationName = itemName || stepNames?.[block.id] || block?.name || ""
$: automationNameError = getAutomationNameError(automationName) $: automationNameError = getAutomationNameError(automationName)
$: status = updateStatus(testResult) $: status = updateStatus(testResult)
@ -135,7 +143,16 @@
{#if isHeaderTrigger} {#if isHeaderTrigger}
<Body size="XS"><b>Trigger</b></Body> <Body size="XS"><b>Trigger</b></Body>
{:else} {:else}
<Body size="XS"><b>{isBranch ? "Branch" : "Step"}</b></Body> <Body size="XS">
<div style="display: flex; gap: 0.5rem; align-items: center;">
<b>{isBranch ? "Branch" : "Step"}</b>
{#if blockDefinition.deprecated}
<Tags>
<Tag invalid>Deprecated</Tag>
</Tags>
{/if}
</div>
</Body>
{/if} {/if}
{#if enableNaming} {#if enableNaming}

View File

@ -102,6 +102,10 @@
if (rowTriggers.includes(trigger?.event)) { if (rowTriggers.includes(trigger?.event)) {
const tableId = trigger?.inputs?.tableId const tableId = trigger?.inputs?.tableId
if (!jsonUpdate.row) {
jsonUpdate.row = {}
}
// Reset the tableId as it must match the trigger // Reset the tableId as it must match the trigger
if (jsonUpdate?.row?.tableId !== tableId) { if (jsonUpdate?.row?.tableId !== tableId) {
jsonUpdate.row.tableId = tableId jsonUpdate.row.tableId = tableId
@ -161,7 +165,7 @@
block={trigger} block={trigger}
on:update={e => { on:update={e => {
const { testData: updatedTestData } = e.detail const { testData: updatedTestData } = e.detail
testData = updatedTestData testData = parseTestData(updatedTestData)
}} }}
/> />
</div> </div>

View File

@ -32,7 +32,6 @@
}) })
$: groupedAutomations = groupAutomations(filteredAutomations) $: groupedAutomations = groupAutomations(filteredAutomations)
$: showNoResults = searchString && !filteredAutomations.length $: showNoResults = searchString && !filteredAutomations.length
const groupAutomations = automations => { const groupAutomations = automations => {
@ -41,7 +40,6 @@
for (let auto of automations) { for (let auto of automations) {
let category = null let category = null
let dataTrigger = false let dataTrigger = false
// Group by datasource if possible // Group by datasource if possible
if (dsTriggers.includes(auto.definition?.trigger?.stepId)) { if (dsTriggers.includes(auto.definition?.trigger?.stepId)) {
if (auto.definition.trigger.inputs?.tableId) { if (auto.definition.trigger.inputs?.tableId) {
@ -97,7 +95,10 @@
{triggerGroup?.name} {triggerGroup?.name}
</div> </div>
{#each triggerGroup.entries as automation} {#each triggerGroup.entries as automation}
<AutomationNavItem {automation} icon={triggerGroup.icon} /> <AutomationNavItem
{automation}
icon={automation?.definition?.trigger?.icon}
/>
{/each} {/each}
</div> </div>
{/each} {/each}

View File

@ -18,10 +18,11 @@
Toggle, Toggle,
Divider, Divider,
Icon, Icon,
CoreSelect,
} from "@budibase/bbui" } from "@budibase/bbui"
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte" import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
import { automationStore, tables } from "@/stores/builder" import { automationStore, tables, evaluationContext } from "@/stores/builder"
import { environment } from "@/stores/portal" import { environment } from "@/stores/portal"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte" import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
import { import {
@ -48,7 +49,13 @@
EditorModes, EditorModes,
} from "@/components/common/CodeEditor" } from "@/components/common/CodeEditor"
import FilterBuilder from "@/components/design/settings/controls/FilterEditor/FilterBuilder.svelte" import FilterBuilder from "@/components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
import { QueryUtils, Utils, search, memo } from "@budibase/frontend-core" import {
QueryUtils,
Utils,
search,
memo,
fetchData,
} from "@budibase/frontend-core"
import { getSchemaForDatasourcePlus } from "@/dataBinding" import { getSchemaForDatasourcePlus } from "@/dataBinding"
import { TriggerStepID, ActionStepID } from "@/constants/backend/automations" import { TriggerStepID, ActionStepID } from "@/constants/backend/automations"
import { onMount, createEventDispatcher } from "svelte" import { onMount, createEventDispatcher } from "svelte"
@ -59,9 +66,13 @@
AutomationStepType, AutomationStepType,
AutomationActionStepId, AutomationActionStepId,
AutomationCustomIOType, AutomationCustomIOType,
SortOrder,
} from "@budibase/types" } from "@budibase/types"
import PropField from "./PropField.svelte" import PropField from "./PropField.svelte"
import { utils } from "@budibase/shared-core" import { utils } from "@budibase/shared-core"
import DrawerBindableCodeEditorField from "@/components/common/bindings/DrawerBindableCodeEditorField.svelte"
import { API } from "@/api"
import InfoDisplay from "@/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
export let automation export let automation
export let block export let block
@ -74,6 +85,7 @@
// Stop unnecessary rendering // Stop unnecessary rendering
const memoBlock = memo(block) const memoBlock = memo(block)
const memoContext = memo({})
const rowTriggers = [ const rowTriggers = [
TriggerStepID.ROW_UPDATED, TriggerStepID.ROW_UPDATED,
@ -95,8 +107,11 @@
let inputData let inputData
let insertAtPos, getCaretPosition let insertAtPos, getCaretPosition
let stepLayouts = {} let stepLayouts = {}
let rowSearchTerm = ""
let selectedRow
$: memoBlock.set(block) $: memoBlock.set(block)
$: memoContext.set($evaluationContext)
$: filters = lookForFilters(schemaProperties) $: filters = lookForFilters(schemaProperties)
$: filterCount = $: filterCount =
@ -109,9 +124,13 @@
$: stepId = $memoBlock.stepId $: stepId = $memoBlock.stepId
$: getInputData(testData, $memoBlock.inputs) $: getInputData(testData, $memoBlock.inputs)
$: tableId = inputData ? inputData.tableId : null $: tableId =
inputData?.row?.tableId ||
testData?.row?.tableId ||
inputData?.tableId ||
null
$: table = tableId $: table = tableId
? $tables.list.find(table => table._id === inputData.tableId) ? $tables.list.find(table => table._id === tableId)
: { schema: {} } : { schema: {} }
$: schema = getSchemaForDatasourcePlus(tableId, { $: schema = getSchemaForDatasourcePlus(tableId, {
searchableSchema: true, searchableSchema: true,
@ -140,6 +159,40 @@
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])] ? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
: [] : []
$: fetch = createFetch({ type: "table", tableId })
$: fetchedRows = $fetch?.rows
$: fetch?.update({
query: {
fuzzy: {
[primaryDisplay]: rowSearchTerm || "",
},
},
})
$: fetchLoading = $fetch?.loading
$: primaryDisplay = table?.primaryDisplay
const createFetch = datasource => {
if (!datasource) {
return
}
return fetchData({
API,
datasource,
options: {
sortColumn: primaryDisplay,
sortOrder: SortOrder.ASCENDING,
query: {
fuzzy: {
[primaryDisplay]: rowSearchTerm || "",
},
},
limit: 20,
},
})
}
const getInputData = (testData, blockInputs) => { const getInputData = (testData, blockInputs) => {
// Test data is not cloned for reactivity // Test data is not cloned for reactivity
let newInputData = testData || cloneDeep(blockInputs) let newInputData = testData || cloneDeep(blockInputs)
@ -167,7 +220,7 @@
const stepStore = writable({}) const stepStore = writable({})
$: stepState = $stepStore?.[block.id] $: stepState = $stepStore?.[block.id]
$: customStepLayouts($memoBlock, schemaProperties, stepState) $: customStepLayouts($memoBlock, schemaProperties, stepState, fetchedRows)
const customStepLayouts = block => { const customStepLayouts = block => {
if ( if (
@ -200,7 +253,6 @@
onChange({ ["revision"]: e.detail }) onChange({ ["revision"]: e.detail })
}, },
updateOnChange: false, updateOnChange: false,
forceModal: true,
}, },
}, },
] ]
@ -228,7 +280,6 @@
onChange({ [rowIdentifier]: e.detail }) onChange({ [rowIdentifier]: e.detail })
}, },
updateOnChange: false, updateOnChange: false,
forceModal: true,
}, },
}, },
] ]
@ -362,6 +413,49 @@
disabled: isTestModal, disabled: isTestModal,
}, },
}, },
{
type: CoreSelect,
title: "Row",
props: {
disabled: !table,
placeholder: "Select a row",
options: fetchedRows,
loading: fetchLoading,
value: selectedRow,
autocomplete: true,
filter: false,
getOptionLabel: row => row?.[primaryDisplay] || "",
compare: (a, b) => a?.[primaryDisplay] === b?.[primaryDisplay],
onChange: e => {
if (isTestModal) {
onChange({
id: e.detail?._id,
revision: e.detail?._rev,
row: e.detail,
oldRow: e.detail,
meta: {
fields: inputData["meta"]?.fields || {},
oldFields: e.detail?.meta?.fields || {},
},
})
}
},
},
},
{
type: InfoDisplay,
props: {
warning: true,
icon: "AlertCircleFilled",
body: `Be careful when testing this automation because your data may be modified or deleted.`,
},
},
{
type: Divider,
props: {
noMargin: true,
},
},
...getIdConfig(), ...getIdConfig(),
...getRevConfig(), ...getRevConfig(),
...getRowTypeConfig(), ...getRowTypeConfig(),
@ -476,6 +570,10 @@
...update, ...update,
}) })
if (!updatedAutomation) {
return
}
// Exclude default or invalid data from the test data // Exclude default or invalid data from the test data
let updatedFields = {} let updatedFields = {}
for (const key of Object.keys(block?.inputs?.fields || {})) { for (const key of Object.keys(block?.inputs?.fields || {})) {
@ -547,7 +645,7 @@
...newTestData, ...newTestData,
body: { body: {
...update, ...update,
...automation.testData?.body, ...(automation?.testData?.body || {}),
}, },
} }
} }
@ -556,6 +654,15 @@
...request, ...request,
} }
if (
newTestData?.row == null ||
Object.keys(newTestData?.row).length === 0
) {
selectedRow = null
} else {
selectedRow = newTestData.row
}
const updatedAuto = const updatedAuto =
automationStore.actions.addTestDataToAutomation(newTestData) automationStore.actions.addTestDataToAutomation(newTestData)
@ -668,6 +775,8 @@
{...config.props} {...config.props}
{bindings} {bindings}
on:change={config.props.onChange} on:change={config.props.onChange}
context={$memoContext}
bind:searchTerm={rowSearchTerm}
/> />
</PropField> </PropField>
{:else} {:else}
@ -676,6 +785,7 @@
{...config.props} {...config.props}
{bindings} {bindings}
on:change={config.props.onChange} on:change={config.props.onChange}
context={$memoContext}
/> />
{/if} {/if}
{/each} {/each}
@ -800,6 +910,7 @@
: "Add signature"} : "Add signature"}
keyPlaceholder={"URL"} keyPlaceholder={"URL"}
valuePlaceholder={"Filename"} valuePlaceholder={"Filename"}
context={$memoContext}
/> />
{:else if isTestModal} {:else if isTestModal}
<ModalBindableInput <ModalBindableInput
@ -824,6 +935,7 @@
? queryLimit ? queryLimit
: ""} : ""}
drawerLeft="260px" drawerLeft="260px"
context={$memoContext}
/> />
{/if} {/if}
</div> </div>
@ -853,6 +965,7 @@
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
showFilterEmptyDropdown={!rowTriggers.includes(stepId)} showFilterEmptyDropdown={!rowTriggers.includes(stepId)}
on:change={e => (tempFilters = e.detail)} on:change={e => (tempFilters = e.detail)}
evaluationContext={$memoContext}
/> />
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
@ -895,7 +1008,19 @@
on:change={e => onChange({ [key]: e.detail })} on:change={e => onChange({ [key]: e.detail })}
value={inputData[key]} value={inputData[key]}
/> />
{:else if value.customType === "code"} {:else if value.customType === "code" && stepId === ActionStepID.EXECUTE_SCRIPT_V2}
<div class="scriptv2-wrapper">
<DrawerBindableCodeEditorField
{bindings}
{schema}
panel={AutomationBindingPanel}
on:change={e => onChange({ [key]: e.detail })}
context={$memoContext}
value={inputData[key]}
/>
</div>
{:else if value.customType === "code" && stepId === ActionStepID.EXECUTE_SCRIPT}
<!-- DEPRECATED -->
<CodeEditorModal <CodeEditorModal
on:hide={() => { on:hide={() => {
// Push any pending changes when the window closes // Push any pending changes when the window closes
@ -977,6 +1102,7 @@
? queryLimit ? queryLimit
: ""} : ""}
drawerLeft="260px" drawerLeft="260px"
context={$memoContext}
/> />
</div> </div>
{/if} {/if}

View File

@ -25,6 +25,7 @@
export let meta export let meta
export let bindings export let bindings
export let isTestModal export let isTestModal
export let context = {}
const typeToField = Object.values(FIELDS).reduce((acc, field) => { const typeToField = Object.values(FIELDS).reduce((acc, field) => {
acc[field.type] = field acc[field.type] = field
@ -58,7 +59,7 @@
$: parsedBindings = bindings.map(binding => { $: parsedBindings = bindings.map(binding => {
let clone = Object.assign({}, binding) let clone = Object.assign({}, binding)
clone.icon = "ShareAndroid" clone.icon = clone.icon ?? "ShareAndroid"
return clone return clone
}) })
@ -258,6 +259,7 @@
fields: editableFields, fields: editableFields,
}} }}
{onChange} {onChange}
{context}
/> />
{:else} {:else}
<DrawerBindableSlot <DrawerBindableSlot
@ -276,6 +278,7 @@
allowJS={true} allowJS={true}
updateOnChange={false} updateOnChange={false}
drawerLeft="260px" drawerLeft="260px"
{context}
> >
<RowSelectorTypes <RowSelectorTypes
{isTestModal} {isTestModal}
@ -286,6 +289,7 @@
meta={{ meta={{
fields: editableFields, fields: editableFields,
}} }}
{context}
onChange={change => onChange(change)} onChange={change => onChange(change)}
/> />
</DrawerBindableSlot> </DrawerBindableSlot>
@ -303,13 +307,22 @@
> >
<ActionButton <ActionButton
icon="Add" icon="Add"
fullWidth
on:click={() => { on:click={() => {
customPopover.show() customPopover.show()
}} }}
disabled={!schemaFields} disabled={!schemaFields}
>Add fields >Add fields
</ActionButton> </ActionButton>
<ActionButton
icon="Remove"
on:click={() => {
dispatch("change", {
meta: { fields: {} },
row: {},
})
}}
>Clear
</ActionButton>
</div> </div>
{/if} {/if}
@ -375,4 +388,11 @@
.prop-control-wrap :global(.icon.json-slot-icon) { .prop-control-wrap :global(.icon.json-slot-icon) {
right: 1px !important; right: 1px !important;
} }
.add-fields-btn {
display: flex;
flex-direction: row;
justify-content: center;
gap: var(--spacing-s);
}
</style> </style>

View File

@ -25,12 +25,13 @@
export let meta export let meta
export let bindings export let bindings
export let isTestModal export let isTestModal
export let context
$: fieldData = value[field] $: fieldData = value[field]
$: parsedBindings = bindings.map(binding => { $: parsedBindings = bindings.map(binding => {
let clone = Object.assign({}, binding) let clone = Object.assign({}, binding)
clone.icon = "ShareAndroid" clone.icon = clone.icon ?? "ShareAndroid"
return clone return clone
}) })
@ -232,6 +233,7 @@
actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE || actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE ||
schema.type === FieldType.SIGNATURE_SINGLE) && schema.type === FieldType.SIGNATURE_SINGLE) &&
fieldData} fieldData}
{context}
/> />
</div> </div>
{:else} {:else}

View File

@ -1,18 +1,11 @@
<script> <script>
import { Input, Select, Button } from "@budibase/bbui" import { Input, Select, Button } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { memo } from "@budibase/frontend-core"
const dispatch = createEventDispatcher() import { generate } from "shortid"
export let value = {} export let value = {}
$: fieldsArray = value
? Object.entries(value).map(([name, type]) => ({
name,
type,
}))
: []
const typeOptions = [ const typeOptions = [
{ {
label: "Text", label: "Text",
@ -36,16 +29,42 @@
}, },
] ]
const dispatch = createEventDispatcher()
const memoValue = memo({ data: {} })
$: memoValue.set({ data: value })
$: fieldsArray = $memoValue.data
? Object.entries($memoValue.data).map(([name, type]) => ({
name,
type,
id: generate(),
}))
: []
function addField() { function addField() {
const newValue = { ...value } const newValue = { ...$memoValue.data }
newValue[""] = "string" newValue[""] = "string"
dispatch("change", newValue) fieldsArray = [...fieldsArray, { name: "", type: "string", id: generate() }]
} }
function removeField(name) { function removeField(idx) {
const newValues = { ...value } const entries = [...fieldsArray]
delete newValues[name]
dispatch("change", newValues) // Remove empty field
if (!entries[idx]?.name) {
fieldsArray.splice(idx, 1)
fieldsArray = [...fieldsArray]
return
}
entries.splice(idx, 1)
const update = entries.reduce((newVals, current) => {
newVals[current.name.trim()] = current.type
return newVals
}, {})
dispatch("change", update)
} }
const fieldNameChanged = originalName => e => { const fieldNameChanged = originalName => e => {
@ -57,11 +76,16 @@
} else { } else {
entries = entries.filter(f => f.name !== originalName) entries = entries.filter(f => f.name !== originalName)
} }
value = entries.reduce((newVals, current) => {
const update = entries
.filter(entry => entry.name)
.reduce((newVals, current) => {
newVals[current.name.trim()] = current.type newVals[current.name.trim()] = current.type
return newVals return newVals
}, {}) }, {})
dispatch("change", value) if (Object.keys(update).length) {
dispatch("change", update)
}
} }
</script> </script>
@ -69,7 +93,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="root"> <div class="root">
<div class="spacer" /> <div class="spacer" />
{#each fieldsArray as field} {#each fieldsArray as field, idx (field.id)}
<div class="field"> <div class="field">
<Input <Input
value={field.name} value={field.name}
@ -88,7 +112,9 @@
/> />
<i <i
class="remove-field ri-delete-bin-line" class="remove-field ri-delete-bin-line"
on:click={() => removeField(field.name)} on:click={() => {
removeField(idx)
}}
/> />
</div> </div>
{/each} {/each}
@ -115,4 +141,12 @@
align-items: center; align-items: center;
gap: var(--spacing-m); gap: var(--spacing-m);
} }
.remove-field {
cursor: pointer;
}
.remove-field:hover {
color: var(--spectrum-global-color-gray-900);
}
</style> </style>

View File

@ -1,132 +0,0 @@
<script>
import { goto } from "@roxi/routify"
import {
keepOpen,
ModalContent,
notifications,
Body,
Layout,
Tabs,
Tab,
Heading,
TextArea,
Dropzone,
} from "@budibase/bbui"
import { datasources, queries } from "@/stores/builder"
import { writable } from "svelte/store"
export let navigateDatasource = false
export let datasourceId
export let createDatasource = false
export let onCancel
const data = writable({
url: "",
raw: "",
file: undefined,
})
let lastTouched = "url"
const getData = async () => {
let dataString
// parse the file into memory and send as string
if (lastTouched === "file") {
dataString = await $data.file.text()
} else if (lastTouched === "url") {
const response = await fetch($data.url)
dataString = await response.text()
} else if (lastTouched === "raw") {
dataString = $data.raw
}
return dataString
}
async function importQueries() {
try {
const dataString = await getData()
if (!datasourceId && !createDatasource) {
throw new Error("No datasource id")
}
const body = {
data: dataString,
datasourceId,
}
const importResult = await queries.import(body)
if (!datasourceId) {
datasourceId = importResult.datasourceId
}
// reload
await datasources.fetch()
await queries.fetch()
if (navigateDatasource) {
$goto(`./datasource/${datasourceId}`)
}
notifications.success(`Imported successfully.`)
} catch (error) {
notifications.error("Error importing queries")
return keepOpen
}
}
</script>
<ModalContent
onConfirm={() => importQueries()}
{onCancel}
confirmText={"Import"}
cancelText="Back"
size="L"
>
<Layout noPadding>
<Heading size="S">Import</Heading>
<Body size="XS"
>Import your rest collection using one of the options below</Body
>
<Tabs selected="File">
<!-- Commenting until nginx csp issue resolved -->
<!-- <Tab title="Link">
<Input
bind:value={$data.url}
on:change={() => (lastTouched = "url")}
label="Enter a URL"
placeholder="e.g. https://petstore.swagger.io/v2/swagger.json"
/>
</Tab> -->
<Tab title="File">
<Dropzone
gallery={false}
value={$data.file ? [$data.file] : []}
on:change={e => {
$data.file = e.detail?.[0]
lastTouched = "file"
}}
fileTags={[
"OpenAPI 3.0",
"OpenAPI 2.0",
"Swagger 2.0",
"cURL",
"YAML",
"JSON",
]}
maximum={1}
/>
</Tab>
<Tab title="Raw Text">
<TextArea
bind:value={$data.raw}
on:change={() => (lastTouched = "raw")}
label={"Paste raw text"}
placeholder={'e.g. curl --location --request GET "https://example.com"'}
/>
</Tab>
</Tabs>
</Layout>
</ModalContent>

View File

@ -43,7 +43,7 @@
const validateDescription = description => { const validateDescription = description => {
if (!description?.length) { if (!description?.length) {
return "Please enter a name" return "Please enter a description"
} }
return null return null
} }

View File

@ -1,3 +1,10 @@
<script context="module" lang="ts">
export const DropdownPosition = {
Relative: "top",
Absolute: "right",
}
</script>
<script lang="ts"> <script lang="ts">
import { Label } from "@budibase/bbui" import { Label } from "@budibase/bbui"
import { onMount, createEventDispatcher, onDestroy } from "svelte" import { onMount, createEventDispatcher, onDestroy } from "svelte"
@ -47,6 +54,7 @@
import { EditorModes } from "./" import { EditorModes } from "./"
import { themeStore } from "@/stores/portal" import { themeStore } from "@/stores/portal"
import type { EditorMode } from "@budibase/types" import type { EditorMode } from "@budibase/types"
import { tooltips } from "@codemirror/view"
import type { BindingCompletion, CodeValidator } from "@/types" import type { BindingCompletion, CodeValidator } from "@/types"
import { validateHbsTemplate } from "./validator/hbs" import { validateHbsTemplate } from "./validator/hbs"
import { validateJsTemplate } from "./validator/js" import { validateJsTemplate } from "./validator/js"
@ -62,11 +70,13 @@
export let jsBindingWrapping = true export let jsBindingWrapping = true
export let readonly = false export let readonly = false
export let readonlyLineNumbers = false export let readonlyLineNumbers = false
export let dropdown = DropdownPosition.Relative
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let textarea: HTMLDivElement let textarea: HTMLDivElement
let editor: EditorView let editor: EditorView
let editorEle: HTMLDivElement
let mounted = false let mounted = false
let isEditorInitialised = false let isEditorInitialised = false
let queuedRefresh = false let queuedRefresh = false
@ -117,7 +127,6 @@
queuedRefresh = true queuedRefresh = true
return return
} }
if ( if (
editor && editor &&
value && value &&
@ -271,16 +280,15 @@
EditorView.inputHandler.of((view, from, to, insert) => { EditorView.inputHandler.of((view, from, to, insert) => {
if (jsBindingWrapping && insert === "$") { if (jsBindingWrapping && insert === "$") {
let { text } = view.state.doc.lineAt(from) let { text } = view.state.doc.lineAt(from)
const left = from ? text.substring(0, from) : "" const left = from ? text.substring(0, from) : ""
const right = to ? text.substring(to) : "" const right = to ? text.substring(to) : ""
const wrap = !left.includes('$("') || !right.includes('")') const wrap =
(!left.includes('$("') || !right.includes('")')) &&
!(left.includes("`") && right.includes("`"))
const anchor = from + (wrap ? 3 : 1)
const tr = view.state.update( const tr = view.state.update(
{ {
changes: [{ from, insert: wrap ? '$("")' : "$" }], changes: [{ from, insert: wrap ? '$("")' : "$" }],
selection: {
anchor: from + (wrap ? 3 : 1),
},
}, },
{ {
scrollIntoView: true, scrollIntoView: true,
@ -288,6 +296,19 @@
} }
) )
view.dispatch(tr) view.dispatch(tr)
// the selection needs to fired after the dispatch - this seems
// to fix an issue with the cursor not moving when the editor is
// first loaded, the first usage of the editor is not ready
// for the anchor to move as well as perform a change
setTimeout(() => {
view.dispatch(
view.state.update({
selection: {
anchor,
},
})
)
}, 1)
return true return true
} }
return false return false
@ -369,14 +390,25 @@
const baseExtensions = buildBaseExtensions() const baseExtensions = buildBaseExtensions()
editor = new EditorView({ editor = new EditorView({
doc: value?.toString(), doc: String(value),
extensions: buildExtensions(baseExtensions), extensions: buildExtensions([
...baseExtensions,
dropdown == DropdownPosition.Absolute
? tooltips({
position: "absolute",
})
: [],
]),
parent: textarea, parent: textarea,
}) })
} }
onMount(async () => { onMount(async () => {
mounted = true mounted = true
// Capture scrolling
editorEle.addEventListener("wheel", e => {
e.stopPropagation()
})
}) })
onDestroy(() => { onDestroy(() => {
@ -391,7 +423,8 @@
<Label size="S">{label}</Label> <Label size="S">{label}</Label>
</div> </div>
{/if} {/if}
<div class={`code-editor ${mode?.name || ""}`}>
<div class={`code-editor ${mode?.name || ""}`} bind:this={editorEle}>
<div tabindex="-1" bind:this={textarea} /> <div tabindex="-1" bind:this={textarea} />
</div> </div>
@ -400,6 +433,7 @@
.code-editor { .code-editor {
font-size: 12px; font-size: 12px;
height: 100%; height: 100%;
cursor: text;
} }
.code-editor :global(.cm-editor) { .code-editor :global(.cm-editor) {
height: 100%; height: 100%;
@ -559,12 +593,11 @@
/* Live binding value / helper container */ /* Live binding value / helper container */
.code-editor :global(.cm-completionInfo) { .code-editor :global(.cm-completionInfo) {
margin-left: var(--spacing-s); margin: 0px var(--spacing-s);
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
background-color: var(--spectrum-global-color-gray-50); background-color: var(--spectrum-global-color-gray-50);
padding: var(--spacing-m); padding: var(--spacing-m);
margin-top: -2px;
} }
/* Wrapper around helpers */ /* Wrapper around helpers */
@ -589,6 +622,7 @@
white-space: pre; white-space: pre;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
overflow-y: auto;
max-height: 480px; max-height: 480px;
} }
.code-editor :global(.binding__example.helper) { .code-editor :global(.binding__example.helper) {

View File

@ -354,7 +354,7 @@
{#if mode === BindingMode.Text} {#if mode === BindingMode.Text}
{#key completions} {#key completions}
<CodeEditor <CodeEditor
value={hbsValue} value={hbsValue || ""}
on:change={onChangeHBSValue} on:change={onChangeHBSValue}
bind:getCaretPosition bind:getCaretPosition
bind:insertAtPos bind:insertAtPos
@ -369,7 +369,7 @@
{:else if mode === BindingMode.JavaScript} {:else if mode === BindingMode.JavaScript}
{#key completions} {#key completions}
<CodeEditor <CodeEditor
value={jsValue ? decodeJSBinding(jsValue) : jsValue} value={jsValue ? decodeJSBinding(jsValue) : ""}
on:change={onChangeJSValue} on:change={onChangeJSValue}
{completions} {completions}
{validations} {validations}

View File

@ -0,0 +1,173 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
import {
decodeJSBinding,
encodeJSBinding,
processObjectSync,
} from "@budibase/string-templates"
import { runtimeToReadableBinding } from "@/dataBinding"
import CodeEditor, { DropdownPosition } from "../CodeEditor/CodeEditor.svelte"
import {
getHelperCompletions,
jsAutocomplete,
snippetAutoComplete,
EditorModes,
bindingsToCompletions,
jsHelperAutocomplete,
} from "../CodeEditor"
import { JsonFormatter } from "@budibase/frontend-core"
import { licensing } from "@/stores/portal"
import type {
EnrichedBinding,
Snippet,
CaretPositionFn,
InsertAtPositionFn,
JSONValue,
} from "@budibase/types"
import type { BindingCompletion, BindingCompletionOption } from "@/types"
import { snippets } from "@/stores/builder"
const dispatch = createEventDispatcher()
export let bindings: EnrichedBinding[] = []
export let value: string = ""
export let allowHelpers = true
export let allowSnippets = true
export let context = null
export let autofocusEditor = false
export let placeholder = null
export let height = 180
let getCaretPosition: CaretPositionFn | undefined
let insertAtPos: InsertAtPositionFn | undefined
$: readable = runtimeToReadableBinding(bindings, value || "")
$: jsValue = decodeJSBinding(readable)
$: useSnippets = allowSnippets && !$licensing.isFreePlan
$: enrichedBindings = enrichBindings(bindings, context, $snippets)
$: editorMode = EditorModes.JS
$: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode)
$: jsCompletions = getJSCompletions(bindingCompletions, $snippets, {
useHelpers: allowHelpers,
useSnippets,
})
const getJSCompletions = (
bindingCompletions: BindingCompletionOption[],
snippets: Snippet[] | null,
config: {
useHelpers: boolean
useSnippets: boolean
}
) => {
const completions: BindingCompletion[] = []
if (bindingCompletions.length) {
completions.push(jsAutocomplete([...bindingCompletions]))
}
if (config.useHelpers) {
completions.push(
jsHelperAutocomplete([...getHelperCompletions(EditorModes.JS)])
)
}
if (config.useSnippets && snippets) {
completions.push(snippetAutoComplete(snippets))
}
return completions
}
const highlightJSON = (json: JSONValue) => {
return JsonFormatter.format(json, {
keyColor: "#e06c75",
numberColor: "#e5c07b",
stringColor: "#98c379",
trueColor: "#d19a66",
falseColor: "#d19a66",
nullColor: "#c678dd",
})
}
const enrichBindings = (
bindings: EnrichedBinding[],
context: any,
snippets: Snippet[] | null
) => {
// Create a single big array to enrich in one go
const bindingStrings = bindings.map(binding => {
if (binding.runtimeBinding.startsWith('trim "')) {
// Account for nasty hardcoded HBS bindings for roles, for legacy
// compatibility
return `{{ ${binding.runtimeBinding} }}`
} else {
return `{{ literal ${binding.runtimeBinding} }}`
}
})
const bindingEvaluations = processObjectSync(bindingStrings, {
...context,
snippets,
})
// Enrich bindings with evaluations and highlighted HTML
return bindings.map((binding, idx) => {
if (!context || typeof bindingEvaluations !== "object") {
return binding
}
const evalObj: Record<any, any> = bindingEvaluations
const value = JSON.stringify(evalObj[idx], null, 2)
return {
...binding,
value,
valueHTML: highlightJSON(value),
}
})
}
const updateValue = (val: any) => {
dispatch("change", val)
}
const onChangeJSValue = (e: { detail: string }) => {
if (!e.detail?.trim()) {
// Don't bother saving empty values as JS
updateValue(null)
} else {
updateValue(encodeJSBinding(e.detail))
}
}
</script>
<div class="code-panel" style="height:{height}px;">
<div class="editor">
{#key jsCompletions}
<CodeEditor
value={jsValue || ""}
on:change={onChangeJSValue}
on:blur
completions={jsCompletions}
mode={EditorModes.JS}
bind:getCaretPosition
bind:insertAtPos
autofocus={autofocusEditor}
placeholder={placeholder ||
"Add bindings by typing $ or use the menu on the right"}
jsBindingWrapping
dropdown={DropdownPosition.Absolute}
/>
{/key}
</div>
</div>
<style>
.code-panel {
display: flex;
}
/* Editor */
.editor {
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
</style>

View File

@ -0,0 +1,68 @@
<script>
import { createEventDispatcher } from "svelte"
import {
ClientBindingPanel,
DrawerBindableSlot,
} from "@/components/common/bindings"
import CodeEditorField from "@/components/common/bindings/CodeEditorField.svelte"
export let value = ""
export let panel = ClientBindingPanel
export let schema = null
export let bindings = []
export let context = {}
export let height = 180
const dispatch = createEventDispatcher()
</script>
<div class="wrapper">
<DrawerBindableSlot
{panel}
{schema}
{value}
{bindings}
{context}
title="Edit Code"
type="longform"
allowJS={true}
allowHBS={false}
updateOnChange={false}
on:change={e => {
value = e.detail
dispatch("change", value)
}}
>
<div class="code-editor-wrapper">
<CodeEditorField
{value}
{bindings}
{context}
{height}
allowHBS={false}
allowJS
placeholder={"Add bindings by typing $"}
on:change={e => (value = e.detail)}
on:blur={() => dispatch("change", value)}
/>
</div>
</DrawerBindableSlot>
</div>
<style>
.wrapper :global(.icon.slot-icon) {
top: 1px;
border-radius: 0 4px 0 4px;
border-right: 0;
border-bottom: 1px solid var(--spectrum-alias-border-color);
}
.wrapper :global(.cm-editor),
.wrapper :global(.cm-scroller) {
border-radius: 4px;
}
.code-editor-wrapper {
box-sizing: border-box;
border: 1px solid var(--spectrum-global-color-gray-400);
border-radius: 4px;
}
</style>

View File

@ -22,6 +22,8 @@
export let updateOnChange = true export let updateOnChange = true
export let type export let type
export let schema export let schema
export let allowHBS = true
export let context = {}
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let bindingDrawer let bindingDrawer
@ -147,7 +149,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="control" class:disabled> <div class="control" class:disabled>
{#if !isValid(value)} {#if !isValid(value) && !$$slots.default}
<Input <Input
{label} {label}
{disabled} {disabled}
@ -171,7 +173,7 @@
{:else} {:else}
<slot /> <slot />
{/if} {/if}
{#if !disabled && type !== "formula" && !disabled && !attachmentTypes.includes(type)} {#if !disabled && type !== "formula" && !attachmentTypes.includes(type)}
<div <div
class={`icon ${getIconClass(value, type)}`} class={`icon ${getIconClass(value, type)}`}
on:click={() => { on:click={() => {
@ -187,7 +189,6 @@
on:drawerShow on:drawerShow
bind:this={bindingDrawer} bind:this={bindingDrawer}
title={title ?? placeholder ?? "Bindings"} title={title ?? placeholder ?? "Bindings"}
forceModal={true}
> >
<Button cta slot="buttons" on:click={saveBinding}>Save</Button> <Button cta slot="buttons" on:click={saveBinding}>Save</Button>
<svelte:component <svelte:component
@ -197,7 +198,9 @@
on:change={event => (tempValue = event.detail)} on:change={event => (tempValue = event.detail)}
{bindings} {bindings}
{allowJS} {allowJS}
{allowHBS}
{allowHelpers} {allowHelpers}
{context}
/> />
</Drawer> </Drawer>
@ -208,22 +211,22 @@
} }
.slot-icon { .slot-icon {
right: 31px !important; right: 31px;
border-right: 1px solid var(--spectrum-alias-border-color); border-right: 1px solid var(--spectrum-alias-border-color);
border-top-right-radius: 0px !important; border-top-right-radius: 0px;
border-bottom-right-radius: 0px !important; border-bottom-right-radius: 0px;
} }
.text-area-slot-icon { .text-area-slot-icon {
border-bottom: 1px solid var(--spectrum-alias-border-color); border-bottom: 1px solid var(--spectrum-alias-border-color);
border-bottom-right-radius: 0px !important; border-bottom-right-radius: 0px;
top: 1px !important; top: 1px;
} }
.json-slot-icon { .json-slot-icon {
border-bottom: 1px solid var(--spectrum-alias-border-color); border-bottom: 1px solid var(--spectrum-alias-border-color);
border-bottom-right-radius: 0px !important; border-bottom-right-radius: 0px;
top: 1px !important; top: 1px;
right: 0px !important; right: 0px;
} }
.icon { .icon {

View File

@ -5,6 +5,7 @@
export let bindings = [] export let bindings = []
export let value = "" export let value = ""
export let allowJS = false export let allowJS = false
export let allowHBS = true
export let context = null export let context = null
$: enrichedBindings = enrichBindings(bindings) $: enrichedBindings = enrichBindings(bindings)
@ -22,8 +23,10 @@
<BindingPanel <BindingPanel
bindings={enrichedBindings} bindings={enrichedBindings}
snippets={$snippets} snippets={$snippets}
allowHelpers
{value} {value}
{allowJS} {allowJS}
{allowHBS}
{context} {context}
on:change on:change
/> />

View File

@ -2,6 +2,7 @@
import { Label, Select, Body } from "@budibase/bbui" import { Label, Select, Body } from "@budibase/bbui"
import { findAllMatchingComponents } from "@/helpers/components" import { findAllMatchingComponents } from "@/helpers/components"
import { selectedScreen } from "@/stores/builder" import { selectedScreen } from "@/stores/builder"
import { InlineAlert } from "@budibase/bbui"
export let parameters export let parameters
@ -27,6 +28,12 @@
<Label small>Table</Label> <Label small>Table</Label>
<Select bind:value={parameters.componentId} options={componentOptions} /> <Select bind:value={parameters.componentId} options={componentOptions} />
</div> </div>
<InlineAlert
header="Legacy action"
message="This action is only compatible with the (deprecated) Table Block. Please see the documentation for further info."
link="https://docs.budibase.com/docs/data-actions#clear-row-selection"
linkText="Budibase Documentation"
/>
</div> </div>
<style> <style>

View File

@ -0,0 +1,41 @@
<script>
import { Label, Checkbox } from "@budibase/bbui"
import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte"
export let parameters
export let bindings = []
</script>
<div class="root">
<Label>Text to copy</Label>
<DrawerBindableInput
title="Text to copy"
{bindings}
value={parameters.textToCopy}
on:change={e => (parameters.textToCopy = e.detail)}
/>
<Label />
<Checkbox text="Show notification" bind:value={parameters.showNotification} />
{#if parameters.showNotification}
<Label>Notification message</Label>
<DrawerBindableInput
title="Notification message"
{bindings}
value={parameters.notificationMessage}
placeholder="Copied to clipboard"
on:change={e => (parameters.notificationMessage = e.detail)}
/>
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: 120px 1fr;
align-items: center;
max-width: 400px;
margin: 0 auto;
}
</style>

View File

@ -26,3 +26,4 @@ export { default as CloseModal } from "./CloseModal.svelte"
export { default as ClearRowSelection } from "./ClearRowSelection.svelte" export { default as ClearRowSelection } from "./ClearRowSelection.svelte"
export { default as DownloadFile } from "./DownloadFile.svelte" export { default as DownloadFile } from "./DownloadFile.svelte"
export { default as RowAction } from "./RowAction.svelte" export { default as RowAction } from "./RowAction.svelte"
export { default as CopyToClipboard } from "./CopyToClipboard.svelte"

View File

@ -183,6 +183,17 @@
"name": "Row Action", "name": "Row Action",
"type": "data", "type": "data",
"component": "RowAction" "component": "RowAction"
},
{
"name": "Copy To Clipboard",
"type": "data",
"component": "CopyToClipboard",
"context": [
{
"label": "Copied text",
"value": "copied"
}
]
} }
] ]
} }

View File

@ -16,6 +16,7 @@
export let datasource export let datasource
export let builderType export let builderType
export let docsURL export let docsURL
export let evaluationContext = {}
</script> </script>
<CoreFilterBuilder <CoreFilterBuilder
@ -32,5 +33,6 @@
{allowOnEmpty} {allowOnEmpty}
{builderType} {builderType}
{docsURL} {docsURL}
{evaluationContext}
on:change on:change
/> />

View File

@ -39,6 +39,7 @@
export let allowJS = false export let allowJS = false
export let actionButtonDisabled = false export let actionButtonDisabled = false
export let compare = (option, value) => option === value export let compare = (option, value) => option === value
export let context = null
let fields = Object.entries(object || {}).map(([name, value]) => ({ let fields = Object.entries(object || {}).map(([name, value]) => ({
name, name,
@ -132,6 +133,7 @@
{allowJS} {allowJS}
{allowHelpers} {allowHelpers}
drawerLeft={bindingDrawerLeft} drawerLeft={bindingDrawerLeft}
{context}
/> />
{:else} {:else}
<Input readonly={readOnly} bind:value={field.name} on:blur={changed} /> <Input readonly={readOnly} bind:value={field.name} on:blur={changed} />
@ -158,6 +160,7 @@
{allowJS} {allowJS}
{allowHelpers} {allowHelpers}
drawerLeft={bindingDrawerLeft} drawerLeft={bindingDrawerLeft}
{context}
/> />
{:else} {:else}
<Input <Input

View File

@ -15,6 +15,7 @@ export const ActionStepID = {
DELETE_ROW: "DELETE_ROW", DELETE_ROW: "DELETE_ROW",
OUTGOING_WEBHOOK: "OUTGOING_WEBHOOK", OUTGOING_WEBHOOK: "OUTGOING_WEBHOOK",
EXECUTE_SCRIPT: "EXECUTE_SCRIPT", EXECUTE_SCRIPT: "EXECUTE_SCRIPT",
EXECUTE_SCRIPT_V2: "EXECUTE_SCRIPT_V2",
EXECUTE_QUERY: "EXECUTE_QUERY", EXECUTE_QUERY: "EXECUTE_QUERY",
SERVER_LOG: "SERVER_LOG", SERVER_LOG: "SERVER_LOG",
DELAY: "DELAY", DELAY: "DELAY",

View File

@ -29,7 +29,12 @@
let modal let modal
let webhookModal let webhookModal
onMount(() => { onMount(async () => {
await automationStore.actions.initAppSelf()
// Init the binding evaluation context
automationStore.actions.initContext()
$automationStore.showTestPanel = false $automationStore.showTestPanel = false
}) })

View File

@ -1,4 +1,4 @@
<script> <script lang="ts">
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { import {
keepOpen, keepOpen,
@ -14,13 +14,14 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { datasources, queries } from "@/stores/builder" import { datasources, queries } from "@/stores/builder"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import type { Datasource } from "@budibase/types"
export let navigateDatasource = false export let navigateDatasource = false
export let datasourceId export let datasourceId: string | undefined = undefined
export let createDatasource = false export let createDatasource = false
export let onCancel export let onCancel: (() => void) | undefined = undefined
const data = writable({ const data = writable<{ url: string; raw: string; file?: any }>({
url: "", url: "",
raw: "", raw: "",
file: undefined, file: undefined,
@ -28,12 +29,14 @@
let lastTouched = "url" let lastTouched = "url"
const getData = async () => { $: datasource = $datasources.selected as Datasource
const getData = async (): Promise<string> => {
let dataString let dataString
// parse the file into memory and send as string // parse the file into memory and send as string
if (lastTouched === "file") { if (lastTouched === "file") {
dataString = await $data.file.text() dataString = await $data.file?.text()
} else if (lastTouched === "url") { } else if (lastTouched === "url") {
const response = await fetch($data.url) const response = await fetch($data.url)
dataString = await response.text() dataString = await response.text()
@ -55,9 +58,9 @@
const body = { const body = {
data: dataString, data: dataString,
datasourceId, datasourceId,
datasource,
} }
const importResult = await queries.importQueries(body)
const importResult = await queries.import(body)
if (!datasourceId) { if (!datasourceId) {
datasourceId = importResult.datasourceId datasourceId = importResult.datasourceId
} }
@ -71,8 +74,8 @@
} }
notifications.success("Imported successfully") notifications.success("Imported successfully")
} catch (error) { } catch (error: any) {
notifications.error("Error importing queries") notifications.error(`Error importing queries - ${error.message}`)
return keepOpen return keepOpen
} }

View File

@ -1,9 +1,9 @@
import { derived, get } from "svelte/store" import { derived, get, readable, Readable } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { generate } from "shortid" import { generate } from "shortid"
import { createHistoryStore, HistoryStore } from "@/stores/builder/history" import { createHistoryStore, HistoryStore } from "@/stores/builder/history"
import { licensing } from "@/stores/portal" import { licensing, organisation, environment } from "@/stores/portal"
import { tables, appStore } from "@/stores/builder" import { tables, appStore } from "@/stores/builder"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { import {
@ -33,9 +33,20 @@ import {
isRowSaveTrigger, isRowSaveTrigger,
isAppTrigger, isAppTrigger,
BranchStep, BranchStep,
GetAutomationTriggerDefinitionsResponse,
GetAutomationActionDefinitionsResponse,
AppSelfResponse,
TestAutomationResponse,
isAutomationResults,
RowActionTriggerOutputs,
WebhookTriggerOutputs,
AutomationCustomIOType,
AutomationTriggerResultOutputs,
AutomationTriggerResult,
AutomationStepType,
} from "@budibase/types" } from "@budibase/types"
import { ActionStepID } from "@/constants/backend/automations" import { ActionStepID, TriggerStepID } from "@/constants/backend/automations"
import { FIELDS } from "@/constants/backend" import { FIELDS as COLUMNS } from "@/constants/backend"
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import { rowActions } from "./rowActions" import { rowActions } from "./rowActions"
import { getNewStepName } from "@/helpers/automations/nameHelpers" import { getNewStepName } from "@/helpers/automations/nameHelpers"
@ -44,10 +55,11 @@ import { BudiStore, DerivedBudiStore } from "@/stores/BudiStore"
interface AutomationState { interface AutomationState {
automations: Automation[] automations: Automation[]
testResults: any | null testResults?: TestAutomationResponse
showTestPanel: boolean showTestPanel: boolean
blockDefinitions: BlockDefinitions blockDefinitions: BlockDefinitions
selectedAutomationId: string | null selectedAutomationId: string | null
appSelf?: AppSelfResponse
} }
interface DerivedAutomationState extends AutomationState { interface DerivedAutomationState extends AutomationState {
@ -57,7 +69,6 @@ interface DerivedAutomationState extends AutomationState {
const initialAutomationState: AutomationState = { const initialAutomationState: AutomationState = {
automations: [], automations: [],
testResults: null,
showTestPanel: false, showTestPanel: false,
blockDefinitions: { blockDefinitions: {
TRIGGER: {}, TRIGGER: {},
@ -68,16 +79,19 @@ const initialAutomationState: AutomationState = {
} }
const getFinalDefinitions = ( const getFinalDefinitions = (
triggers: Record<string, any>, triggers: GetAutomationTriggerDefinitionsResponse,
actions: Record<string, any> actions: GetAutomationActionDefinitionsResponse
): BlockDefinitions => { ): BlockDefinitions => {
const creatable: Record<string, any> = {} const creatable: Partial<GetAutomationTriggerDefinitionsResponse> = {}
Object.entries(triggers).forEach(entry => { for (const [key, trigger] of Object.entries(triggers)) {
if (entry[0] === AutomationTriggerStepId.ROW_ACTION) { if (key === AutomationTriggerStepId.ROW_ACTION) {
return continue
}
if (trigger.deprecated === true) {
continue
}
creatable[key as keyof GetAutomationTriggerDefinitionsResponse] = trigger
} }
creatable[entry[0]] = entry[1]
})
return { return {
TRIGGER: triggers, TRIGGER: triggers,
CREATABLE_TRIGGER: creatable, CREATABLE_TRIGGER: creatable,
@ -86,6 +100,116 @@ const getFinalDefinitions = (
} }
const automationActions = (store: AutomationStore) => ({ const automationActions = (store: AutomationStore) => ({
/**
* Generates a derived store acting as an evaluation context
* for bindings in automations
*
* @returns {Readable<AutomationContext>}
*/
generateContext: (): Readable<AutomationContext> => {
return derived(
[organisation, store.selected, environment, tables],
([$organisation, $selectedAutomation, $env, $tables]) => {
const { platformUrl: url, company, logoUrl: logo } = $organisation
const results: TestAutomationResponse | undefined =
$selectedAutomation?.testResults
const testData: AutomationTriggerResultOutputs | undefined =
$selectedAutomation.data?.testData
const triggerDef = $selectedAutomation.data?.definition?.trigger
const isWebhook = triggerDef?.stepId === TriggerStepID.WEBHOOK
const isRowAction = triggerDef?.stepId === TriggerStepID.ROW_ACTION
const rowActionTableId = triggerDef?.inputs?.tableId
const rowActionTable = rowActionTableId
? $tables.list.find(table => table._id === rowActionTableId)
: undefined
let triggerData: AutomationTriggerResultOutputs | undefined
if (results && isAutomationResults(results)) {
const automationTrigger: AutomationTriggerResult | undefined =
results?.trigger
const outputs: AutomationTriggerResultOutputs | undefined =
automationTrigger?.outputs
triggerData = outputs ? outputs : undefined
if (triggerData) {
if (isRowAction && rowActionTable) {
const rowTrigger = triggerData as RowActionTriggerOutputs
// Row action table must always be retrieved as it is never
// returned in the test results
rowTrigger.table = rowActionTable
} else if (isWebhook) {
const webhookTrigger = triggerData as WebhookTriggerOutputs
// Ensure it displays in the event that the configuration have been skipped
webhookTrigger.body = webhookTrigger.body ?? {}
}
}
// Clean up unnecessary data from the context
// Meta contains UI/UX config data. Non-bindable
delete triggerData?.meta
} else {
// Substitute test data in place of the trigger data if the test hasn't been run
triggerData = testData
}
// AppSelf context required to mirror server user context
const userContext = $selectedAutomation.appSelf || {}
// Extract step results from a valid response
const stepResults =
results && isAutomationResults(results) ? results?.steps : []
return {
user: userContext,
// Merge in the trigger data.
...(triggerData ? { trigger: { ...triggerData } } : {}),
// This will initially be empty for each step but will populate
// upon running the test.
steps: stepResults.reduce(
(acc: Record<string, any>, res: Record<string, any>) => {
acc[res.id] = res.outputs
return acc
},
{}
),
env: ($env?.variables || []).reduce(
(acc: Record<string, any>, variable: Record<string, any>) => {
acc[variable.name] = ""
return acc
},
{}
),
settings: { url, company, logo },
}
}
)
},
/**
* Initialise the automation evaluation context
*/
initContext: () => {
store.context = store.actions.generateContext()
},
/**
* Fetches the app user context used for live evaluation
* This matches the context used on the server
* @returns {AppSelfResponse | null}
*/
initAppSelf: async (): Promise<AppSelfResponse | null> => {
// Fetch and update the app self if it hasn't been set
const appSelfResponse = await API.fetchSelf()
store.update(state => ({
...state,
...(appSelfResponse ? { appSelf: appSelfResponse } : {}),
}))
return appSelfResponse
},
/** /**
* Move a given block from one location on the tree to another. * Move a given block from one location on the tree to another.
* *
@ -282,9 +406,12 @@ const automationActions = (store: AutomationStore) => ({
* Build a sequential list of all steps on the step path provided * Build a sequential list of all steps on the step path provided
* *
* @param {Array<Object>} pathWay e.g. [{stepIdx:2},{branchIdx:0, stepIdx:2},...] * @param {Array<Object>} pathWay e.g. [{stepIdx:2},{branchIdx:0, stepIdx:2},...]
* @returns {Array<Object>} all steps encountered on the provided path * @returns {Array<AutomationStep | AutomationTrigger>} all steps encountered on the provided path
*/ */
getPathSteps: (pathWay: Array<BranchPath>, automation: Automation) => { getPathSteps: (
pathWay: Array<BranchPath>,
automation: Automation
): Array<AutomationStep | AutomationTrigger> => {
// Base Steps, including trigger // Base Steps, including trigger
const steps = [ const steps = [
automation.definition.trigger, automation.definition.trigger,
@ -531,41 +658,72 @@ const automationActions = (store: AutomationStore) => ({
let bindings: any[] = [] let bindings: any[] = []
const addBinding = ( const addBinding = (
name: string, name: string,
value: any, schema: any,
icon: string, icon: string,
idx: number, idx: number,
isLoopBlock: boolean, isLoopBlock: boolean,
bindingName?: string pathBlock: AutomationStep | AutomationTrigger,
bindingName: string
) => { ) => {
if (!name) return if (!name) return
const runtimeBinding = store.actions.determineRuntimeBinding( const runtimeBinding = store.actions.determineRuntimeBinding(
name, name,
idx, idx,
isLoopBlock, isLoopBlock,
bindingName,
automation, automation,
currentBlock, currentBlock,
pathSteps pathSteps
) )
// Skip binding if its invalid
if (!runtimeBinding) {
return
}
const readableBinding = store.actions.determineReadableBinding(
name,
pathBlock
)
const categoryName = store.actions.determineCategoryName( const categoryName = store.actions.determineCategoryName(
idx, idx,
isLoopBlock, isLoopBlock,
bindingName, bindingName,
loopBlockCount loopBlockCount
) )
bindings.push(
store.actions.createBindingObject( const isStep = !isLoopBlock && idx !== 0
name, const defaultReadable =
value, bindingName && isStep ? `steps.${bindingName}.${name}` : runtimeBinding
icon,
idx, // Check if the schema matches any column types.
loopBlockCount, const column = Object.values(COLUMNS).find(
isLoopBlock, col =>
col.type === schema.type &&
("subtype" in col ? col.subtype === schema.subtype : true)
)
// Automation types and column types can collide e.g. "array"
// Exclude where necessary
const ignoreColumnType = schema.customType === AutomationCustomIOType.ROWS
// Shown in the bindable menus
const displayType = ignoreColumnType ? schema.type : column?.name
bindings.push({
readableBinding: readableBinding || defaultReadable,
runtimeBinding, runtimeBinding,
categoryName, type: schema.type,
bindingName description: schema.description,
) icon,
) category: categoryName,
display: {
type: displayType,
name,
rank: isLoopBlock ? idx + 1 : idx - loopBlockCount,
},
})
} }
let loopBlockCount = 0 let loopBlockCount = 0
@ -633,8 +791,17 @@ const automationActions = (store: AutomationStore) => ({
console.error("Loop block missing.") console.error("Loop block missing.")
} }
} }
Object.entries(schema).forEach(([name, value]) => { Object.entries(schema).forEach(([name, value]) => {
addBinding(name, value, icon, blockIdx, isLoopBlock, bindingName) addBinding(
name,
value,
icon,
blockIdx,
isLoopBlock,
pathBlock,
bindingName
)
}) })
} }
@ -645,23 +812,60 @@ const automationActions = (store: AutomationStore) => ({
return bindings return bindings
}, },
determineReadableBinding: (
name: string,
block: AutomationStep | AutomationTrigger
) => {
const rowTriggers = [
TriggerStepID.ROW_UPDATED,
TriggerStepID.ROW_SAVED,
TriggerStepID.ROW_DELETED,
TriggerStepID.ROW_ACTION,
]
const isTrigger = block.type === AutomationStepType.TRIGGER
const isAppTrigger = block.stepId === AutomationTriggerStepId.APP
const isRowTrigger = rowTriggers.includes(block.stepId)
let readableBinding = ""
if (isTrigger) {
if (isAppTrigger) {
readableBinding = `trigger.fields.${name}`
} else if (isRowTrigger) {
let noRowKeywordBindings = ["id", "revision", "oldRow"]
readableBinding = noRowKeywordBindings.includes(name)
? `trigger.${name}`
: `trigger.row.${name}`
} else {
readableBinding = `trigger.${name}`
}
}
return readableBinding
},
determineRuntimeBinding: ( determineRuntimeBinding: (
name: string, name: string,
idx: number, idx: number,
isLoopBlock: boolean, isLoopBlock: boolean,
bindingName: string | undefined,
automation: Automation, automation: Automation,
currentBlock: AutomationStep | AutomationTrigger | undefined, currentBlock: AutomationStep | AutomationTrigger | undefined,
pathSteps: (AutomationStep | AutomationTrigger)[] pathSteps: (AutomationStep | AutomationTrigger)[]
) => { ) => {
let runtimeName: string | null let runtimeName: string
// Legacy support for EXECUTE_SCRIPT steps
const isJSScript =
currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT
/* Begin special cases for generating custom schemas based on triggers */ /* Begin special cases for generating custom schemas based on triggers */
if ( if (
idx === 0 && idx === 0 &&
automation.definition.trigger?.event === AutomationEventType.APP_TRIGGER automation.definition.trigger?.event === AutomationEventType.APP_TRIGGER
) { ) {
return `trigger.fields.${name}` return isJSScript
? `trigger.fields["${name}"]`
: `trigger.fields.[${name}]`
} }
if ( if (
@ -671,26 +875,28 @@ const automationActions = (store: AutomationStore) => ({
automation.definition.trigger?.event === AutomationEventType.ROW_SAVE) automation.definition.trigger?.event === AutomationEventType.ROW_SAVE)
) { ) {
let noRowKeywordBindings = ["id", "revision", "oldRow"] let noRowKeywordBindings = ["id", "revision", "oldRow"]
if (!noRowKeywordBindings.includes(name)) return `trigger.row.${name}` if (!noRowKeywordBindings.includes(name)) {
return isJSScript ? `trigger.row["${name}"]` : `trigger.row.[${name}]`
}
} }
/* End special cases for generating custom schemas based on triggers */ /* End special cases for generating custom schemas based on triggers */
if (isLoopBlock) { if (isLoopBlock) {
runtimeName = `loop.${name}` runtimeName = `loop.${name}`
} else if (idx === 0) { } else if (idx === 0) {
runtimeName = `trigger.${name}` runtimeName = `trigger.[${name}]`
} else if (currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT) { } else if (isJSScript) {
const stepId = pathSteps[idx].id const stepId = pathSteps[idx].id
if (!stepId) { if (!stepId) {
notifications.error("Error generating binding: Step ID not found.") notifications.error("Error generating binding: Step ID not found.")
return null return
} }
runtimeName = `steps["${stepId}"].${name}` runtimeName = `steps["${stepId}"].${name}`
} else { } else {
const stepId = pathSteps[idx].id const stepId = pathSteps[idx].id
if (!stepId) { if (!stepId) {
notifications.error("Error generating binding: Step ID not found.") notifications.error("Error generating binding: Step ID not found.")
return null return
} }
runtimeName = `steps.${stepId}.${name}` runtimeName = `steps.${stepId}.${name}`
} }
@ -711,40 +917,6 @@ const automationActions = (store: AutomationStore) => ({
: `Step ${idx - loopBlockCount} outputs` : `Step ${idx - loopBlockCount} outputs`
}, },
createBindingObject: (
name: string,
value: any,
icon: string,
idx: number,
loopBlockCount: number,
isLoopBlock: boolean,
runtimeBinding: string | null,
categoryName: string,
bindingName?: string
) => {
const field = Object.values(FIELDS).find(
field =>
field.type === value.type &&
("subtype" in field ? field.subtype === value.subtype : true)
)
return {
readableBinding:
bindingName && !isLoopBlock && idx !== 0
? `steps.${bindingName}.${name}`
: runtimeBinding,
runtimeBinding,
type: value.type,
description: value.description,
icon,
category: categoryName,
display: {
type: field?.name || value.type,
name,
rank: isLoopBlock ? idx + 1 : idx - loopBlockCount,
},
}
},
processBlockInputs: async ( processBlockInputs: async (
block: AutomationStep, block: AutomationStep,
data: Record<string, any> data: Record<string, any>
@ -796,19 +968,14 @@ const automationActions = (store: AutomationStore) => ({
}, },
test: async (automation: Automation, testData: any) => { test: async (automation: Automation, testData: any) => {
let result: any let result: TestAutomationResponse
try { try {
result = await API.testAutomation(automation._id!, testData) result = await API.testAutomation(automation._id!, testData)
} catch (err: any) { } catch (err: any) {
const message = err.message || err.status || JSON.stringify(err) const message = err.message || err.status || JSON.stringify(err)
throw `Automation test failed - ${message}` throw `Automation test failed - ${message}`
} }
if (!result?.trigger && !result?.steps?.length && !result?.message) {
if (result?.err?.code === "usage_limit_exceeded") {
throw "You have exceeded your automation quota"
}
throw "Something went wrong testing your automation"
}
store.update(state => { store.update(state => {
state.testResults = result state.testResults = result
return state return state
@ -1396,7 +1563,7 @@ const automationActions = (store: AutomationStore) => ({
} }
store.update(state => { store.update(state => {
state.selectedAutomationId = id state.selectedAutomationId = id
state.testResults = null delete state.testResults
state.showTestPanel = false state.showTestPanel = false
return state return state
}) })
@ -1436,29 +1603,14 @@ const automationActions = (store: AutomationStore) => ({
}, },
}) })
class AutomationStore extends BudiStore<AutomationState> { export interface AutomationContext {
history: HistoryStore<Automation> user: AppSelfResponse | null
actions: ReturnType<typeof automationActions> trigger?: AutomationTriggerResultOutputs
steps: Record<string, AutomationStep>
constructor() { env: Record<string, any>
super(initialAutomationState) settings: Record<string, any>
this.actions = automationActions(this)
this.history = createHistoryStore({
getDoc: this.actions.getDefinition.bind(this),
selectDoc: this.actions.select.bind(this),
})
// Then wrap save and delete with history
const originalSave = this.actions.save.bind(this.actions)
const originalDelete = this.actions.delete.bind(this.actions)
this.actions.save = this.history.wrapSaveDoc(originalSave)
this.actions.delete = this.history.wrapDeleteDoc(originalDelete)
}
} }
export const automationStore = new AutomationStore()
export const automationHistoryStore = automationStore.history
export class SelectedAutomationStore extends DerivedBudiStore< export class SelectedAutomationStore extends DerivedBudiStore<
AutomationState, AutomationState,
DerivedAutomationState DerivedAutomationState
@ -1519,4 +1671,49 @@ export class SelectedAutomationStore extends DerivedBudiStore<
super(initialAutomationState, makeDerivedStore) super(initialAutomationState, makeDerivedStore)
} }
} }
export const selectedAutomation = new SelectedAutomationStore(automationStore)
class AutomationStore extends BudiStore<AutomationState> {
history: HistoryStore<Automation>
actions: ReturnType<typeof automationActions>
selected: SelectedAutomationStore
context: Readable<AutomationContext> | undefined
constructor() {
super(initialAutomationState)
this.actions = automationActions(this)
this.history = createHistoryStore({
getDoc: this.actions.getDefinition.bind(this),
selectDoc: this.actions.select.bind(this),
})
// Then wrap save and delete with history
const originalSave = this.actions.save.bind(this.actions)
const originalDelete = this.actions.delete.bind(this.actions)
this.actions.save = this.history.wrapSaveDoc(originalSave)
this.actions.delete = this.history.wrapDeleteDoc(originalDelete)
this.selected = new SelectedAutomationStore(this)
}
}
export const automationStore = new AutomationStore()
export const automationHistoryStore = automationStore.history
export const selectedAutomation = automationStore.selected
// Define an empty evaluate context at the start
const emptyContext: AutomationContext = {
user: {},
steps: {},
env: {},
settings: {},
}
// Page layout kicks off initialisation, subscription happens within the page
export const evaluationContext: Readable<AutomationContext> = readable(
emptyContext,
set => {
const unsubscribe = automationStore.context?.subscribe(set) ?? (() => {})
return () => unsubscribe()
}
)

View File

@ -11,6 +11,7 @@ import {
automationStore, automationStore,
selectedAutomation, selectedAutomation,
automationHistoryStore, automationHistoryStore,
evaluationContext,
} from "./automations" } from "./automations"
import { userStore, userSelectedResourceMap, isOnlyUser } from "./users" import { userStore, userSelectedResourceMap, isOnlyUser } from "./users"
import { deploymentStore } from "./deployments" import { deploymentStore } from "./deployments"
@ -72,6 +73,7 @@ export {
snippets, snippets,
rowActions, rowActions,
appPublished, appPublished,
evaluationContext,
screenComponentsList, screenComponentsList,
screenComponentErrors, screenComponentErrors,
screenComponentErrorList, screenComponentErrorList,

View File

@ -16,7 +16,8 @@
}, },
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"dev": "vite build --watch --mode=dev" "dev": "vite build --watch --mode=dev",
"check:types": "yarn svelte-check"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "*", "@budibase/bbui": "*",

View File

@ -10,7 +10,9 @@ export const API = createAPIClient({
// Attach client specific headers // Attach client specific headers
attachHeaders: headers => { attachHeaders: headers => {
// Attach app ID header // Attach app ID header
if (window["##BUDIBASE_APP_ID##"]) {
headers["x-budibase-app-id"] = window["##BUDIBASE_APP_ID##"] headers["x-budibase-app-id"] = window["##BUDIBASE_APP_ID##"]
}
// Attach client header if not inside the builder preview // Attach client header if not inside the builder preview
if (!window["##BUDIBASE_IN_BUILDER##"]) { if (!window["##BUDIBASE_IN_BUILDER##"]) {

View File

@ -141,6 +141,7 @@
var(--spectrum-global-dimension-size-300) var(--spectrum-global-dimension-size-300)
); );
display: -webkit-box; display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;

View File

@ -4,12 +4,12 @@
import { Utils } from "@budibase/frontend-core" import { Utils } from "@budibase/frontend-core"
import FormBlockWrapper from "./FormBlockWrapper.svelte" import FormBlockWrapper from "./FormBlockWrapper.svelte"
import { get } from "svelte/store" import { get } from "svelte/store"
import type { TableSchema, UIDatasource } from "@budibase/types" import type { TableSchema } from "@budibase/types"
type Field = { name: string; active: boolean } type Field = { name: string; active: boolean }
export let actionType: string export let actionType: string
export let dataSource: UIDatasource export let dataSource: { resourceId: string }
export let size: string export let size: string
export let disabled: boolean export let disabled: boolean
export let fields: (Field | string)[] export let fields: (Field | string)[]
@ -30,8 +30,8 @@
// Legacy // Legacy
export let showDeleteButton: boolean export let showDeleteButton: boolean
export let showSaveButton: boolean export let showSaveButton: boolean
export let saveButtonLabel: boolean export let saveButtonLabel: string
export let deleteButtonLabel: boolean export let deleteButtonLabel: string
const { fetchDatasourceSchema, generateGoldenSample } = getContext("sdk") const { fetchDatasourceSchema, generateGoldenSample } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
@ -107,7 +107,7 @@
return [...fields, ...defaultFields].filter(field => field.active) return [...fields, ...defaultFields].filter(field => field.active)
} }
const fetchSchema = async (datasource: UIDatasource) => { const fetchSchema = async (datasource: { resourceId: string }) => {
schema = (await fetchDatasourceSchema(datasource)) || {} schema = (await fetchDatasourceSchema(datasource)) || {}
} }
</script> </script>

View File

@ -7,6 +7,7 @@
import { isGridEvent } from "@/utils/grid" import { isGridEvent } from "@/utils/grid"
import { DNDPlaceholderID } from "@/constants" import { DNDPlaceholderID } from "@/constants"
import type { Component } from "@budibase/types" import type { Component } from "@budibase/types"
import { DropPosition } from "@budibase/types"
type ChildCoords = { type ChildCoords = {
placeholder: boolean placeholder: boolean
@ -287,7 +288,7 @@
} }
// Convert parent + index into target + mode // Convert parent + index into target + mode
let legacyDropTarget, legacyDropMode let legacyDropTarget, legacyDropMode: DropPosition
const parent: Component | null = findComponentById( const parent: Component | null = findComponentById(
get(screenStore).activeScreen?.props, get(screenStore).activeScreen?.props,
drop.parent drop.parent
@ -309,16 +310,16 @@
// Use inside if no existing children // Use inside if no existing children
if (!children?.length) { if (!children?.length) {
legacyDropTarget = parent._id legacyDropTarget = parent._id
legacyDropMode = "inside" legacyDropMode = DropPosition.INSIDE
} else if (drop.index === 0) { } else if (drop.index === 0) {
legacyDropTarget = children[0]?._id legacyDropTarget = children[0]?._id
legacyDropMode = "above" legacyDropMode = DropPosition.ABOVE
} else { } else {
legacyDropTarget = children[drop.index - 1]?._id legacyDropTarget = children[drop.index - 1]?._id
legacyDropMode = "below" legacyDropMode = DropPosition.BELOW
} }
if (legacyDropTarget && legacyDropMode) { if (legacyDropTarget && legacyDropMode && source.id) {
dropping = true dropping = true
await builderStore.actions.moveComponent( await builderStore.actions.moveComponent(
source.id, source.id,

View File

@ -17,6 +17,7 @@
Devices, Devices,
GridDragMode, GridDragMode,
} from "@/utils/grid" } from "@/utils/grid"
import { DropPosition } from "@budibase/types"
type GridDragSide = type GridDragSide =
| "top" | "top"
@ -222,7 +223,7 @@
// If holding ctrl/cmd then leave behind a duplicate of this component // If holding ctrl/cmd then leave behind a duplicate of this component
if (mode === GridDragMode.Move && (e.ctrlKey || e.metaKey)) { if (mode === GridDragMode.Move && (e.ctrlKey || e.metaKey)) {
builderStore.actions.duplicateComponent(id, "above", false) builderStore.actions.duplicateComponent(id, DropPosition.ABOVE, false)
} }
// Find grid parent and read from DOM // Find grid parent and read from DOM

View File

@ -115,7 +115,7 @@ const createBuilderStore = () => {
component: string, component: string,
parent: string, parent: string,
index: number, index: number,
props: Record<string, any> props?: Record<string, any>
) => { ) => {
eventStore.actions.dispatchEvent("drop-new-component", { eventStore.actions.dispatchEvent("drop-new-component", {
component, component,

View File

@ -421,6 +421,28 @@ const showNotificationHandler = action => {
const promptUserHandler = () => {} const promptUserHandler = () => {}
const copyToClipboardHandler = async action => {
const { textToCopy, showNotification, notificationMessage } =
action.parameters
if (!textToCopy) {
return
}
try {
await navigator.clipboard.writeText(textToCopy)
if (showNotification) {
const message = notificationMessage || "Copied to clipboard"
notificationStore.actions.success(message, true, 3000)
}
} catch (err) {
console.error("Failed to copy text: ", err)
notificationStore.actions.error("Failed to copy to clipboard")
}
return { copied: textToCopy }
}
const openSidePanelHandler = action => { const openSidePanelHandler = action => {
const { id } = action.parameters const { id } = action.parameters
if (id) { if (id) {
@ -514,6 +536,7 @@ const handlerMap = {
["Close Modal"]: closeModalHandler, ["Close Modal"]: closeModalHandler,
["Download File"]: downloadFileHandler, ["Download File"]: downloadFileHandler,
["Row Action"]: rowActionHandler, ["Row Action"]: rowActionHandler,
["Copy To Clipboard"]: copyToClipboardHandler,
} }
const confirmTextMap = { const confirmTextMap = {

View File

@ -7,6 +7,7 @@
"target": "ESNext", "target": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true,
"paths": { "paths": {
"@budibase/*": [ "@budibase/*": [
"../*/src/index.ts", "../*/src/index.ts",

View File

@ -16,8 +16,5 @@
"lodash": "4.17.21", "lodash": "4.17.21",
"shortid": "2.2.15", "shortid": "2.2.15",
"socket.io-client": "^4.7.5" "socket.io-client": "^4.7.5"
},
"devDependencies": {
"svelte-check": "^4.1.0"
} }
} }

View File

@ -10,6 +10,7 @@
export let drawerTitle export let drawerTitle
export let toReadable export let toReadable
export let toRuntime export let toRuntime
export let evaluationContext = {}
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -66,7 +67,6 @@
> >
Confirm Confirm
</Button> </Button>
<svelte:component <svelte:component
this={panel} this={panel}
slot="body" slot="body"
@ -76,6 +76,7 @@
allowHBS allowHBS
on:change={drawerOnChange} on:change={drawerOnChange}
{bindings} {bindings}
context={evaluationContext}
/> />
</Drawer> </Drawer>

View File

@ -42,6 +42,7 @@
export let panel export let panel
export let toReadable export let toReadable
export let toRuntime export let toRuntime
export let evaluationContext = {}
$: editableFilters = migrateFilters(filters) $: editableFilters = migrateFilters(filters)
$: { $: {
@ -385,6 +386,7 @@
{panel} {panel}
{toReadable} {toReadable}
{toRuntime} {toRuntime}
{evaluationContext}
on:change={e => { on:change={e => {
const updated = { const updated = {
...filter, ...filter,
@ -423,6 +425,7 @@
{panel} {panel}
{toReadable} {toReadable}
{toRuntime} {toRuntime}
{evaluationContext}
on:change={e => { on:change={e => {
onFilterFieldUpdate( onFilterFieldUpdate(
{ ...filter, ...e.detail }, { ...filter, ...e.detail },

View File

@ -24,6 +24,7 @@
export let drawerTitle export let drawerTitle
export let toReadable export let toReadable
export let toRuntime export let toRuntime
export let evaluationContext = {}
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const { OperatorOptions, FilterValueType } = Constants const { OperatorOptions, FilterValueType } = Constants
@ -156,6 +157,7 @@
allowHBS allowHBS
on:change={drawerOnChange} on:change={drawerOnChange}
{bindings} {bindings}
context={evaluationContext}
/> />
</Drawer> </Drawer>

View File

@ -8,7 +8,6 @@ import {
Row, Row,
SearchFilters, SearchFilters,
SortOrder, SortOrder,
SortType,
TableSchema, TableSchema,
} from "@budibase/types" } from "@budibase/types"
import { APIClient } from "../api/types" import { APIClient } from "../api/types"
@ -72,8 +71,6 @@ export default abstract class BaseDataFetch<
options: DataFetchOptions<TQuery> & { options: DataFetchOptions<TQuery> & {
datasource: TDatasource datasource: TDatasource
sortType: SortType | null
// Client side feature customisation // Client side feature customisation
clientSideSearching: boolean clientSideSearching: boolean
clientSideSorting: boolean clientSideSorting: boolean
@ -106,7 +103,6 @@ export default abstract class BaseDataFetch<
// Sorting config // Sorting config
sortColumn: null, sortColumn: null,
sortOrder: SortOrder.ASCENDING, sortOrder: SortOrder.ASCENDING,
sortType: null,
// Pagination config // Pagination config
paginate: true, paginate: true,
@ -227,31 +223,12 @@ export default abstract class BaseDataFetch<
this.options.sortColumn = this.getDefaultSortColumn(definition, schema) this.options.sortColumn = this.getDefaultSortColumn(definition, schema)
} }
// If we don't have a sort column specified then just ensure we don't set
// any sorting params
if (!this.options.sortColumn) {
this.options.sortOrder = SortOrder.ASCENDING
this.options.sortType = null
} else {
// Otherwise determine what sort type to use base on sort column
this.options.sortType = SortType.STRING
const fieldSchema = schema?.[this.options.sortColumn]
if (
fieldSchema?.type === FieldType.NUMBER ||
fieldSchema?.type === FieldType.BIGINT ||
("calculationType" in fieldSchema && fieldSchema?.calculationType)
) {
this.options.sortType = SortType.NUMBER
}
// If no sort order, default to ascending // If no sort order, default to ascending
if (!this.options.sortOrder) { if (!this.options.sortOrder) {
this.options.sortOrder = SortOrder.ASCENDING this.options.sortOrder = SortOrder.ASCENDING
} else { } else {
// Ensure sortOrder matches the enum // Ensure sortOrder matches the enum
this.options.sortOrder = this.options.sortOrder = this.options.sortOrder.toLowerCase() as SortOrder
this.options.sortOrder.toLowerCase() as SortOrder
}
} }
// Build the query // Build the query
@ -294,7 +271,6 @@ export default abstract class BaseDataFetch<
const { const {
sortColumn, sortColumn,
sortOrder, sortOrder,
sortType,
limit, limit,
clientSideSearching, clientSideSearching,
clientSideSorting, clientSideSorting,
@ -311,8 +287,8 @@ export default abstract class BaseDataFetch<
} }
// If we don't support sorting, do a client-side sort // If we don't support sorting, do a client-side sort
if (!this.features.supportsSort && clientSideSorting && sortType) { if (!this.features.supportsSort && clientSideSorting && sortColumn) {
rows = sort(rows, sortColumn as any, sortOrder, sortType) rows = sort(rows, sortColumn, sortOrder)
} }
// If we don't support pagination, do a client-side limit // If we don't support pagination, do a client-side limit

View File

@ -29,8 +29,7 @@ export default class TableFetch extends BaseDataFetch<TableDatasource, Table> {
} }
async getData() { async getData() {
const { datasource, limit, sortColumn, sortOrder, sortType, paginate } = const { datasource, limit, sortColumn, sortOrder, paginate } = this.options
this.options
const { tableId } = datasource const { tableId } = datasource
const { cursor, query } = get(this.store) const { cursor, query } = get(this.store)
@ -41,7 +40,6 @@ export default class TableFetch extends BaseDataFetch<TableDatasource, Table> {
limit, limit,
sort: sortColumn, sort: sortColumn,
sortOrder: sortOrder ?? SortOrder.ASCENDING, sortOrder: sortOrder ?? SortOrder.ASCENDING,
sortType,
paginate, paginate,
bookmark: cursor, bookmark: cursor,
}) })

View File

@ -1,4 +1,5 @@
import { import {
SearchViewRowRequest,
SortOrder, SortOrder,
ViewDatasource, ViewDatasource,
ViewV2Enriched, ViewV2Enriched,
@ -40,8 +41,7 @@ export default class ViewV2Fetch extends BaseDataFetch<
} }
async getData() { async getData() {
const { datasource, limit, sortColumn, sortOrder, sortType, paginate } = const { datasource, limit, sortColumn, sortOrder, paginate } = this.options
this.options
const { cursor, query, definition } = get(this.store) const { cursor, query, definition } = get(this.store)
// If this is a calculation view and we have no calculations, return nothing // If this is a calculation view and we have no calculations, return nothing
@ -68,14 +68,13 @@ export default class ViewV2Fetch extends BaseDataFetch<
} }
try { try {
const request = { const request: SearchViewRowRequest = {
query, query,
paginate, paginate,
limit, limit,
bookmark: cursor, bookmark: cursor,
sort: sortColumn, sort: sortColumn,
sortOrder: sortOrder, sortOrder: sortOrder,
sortType,
} }
if (paginate) { if (paginate) {
const res = await this.API.viewV2.fetch(datasource.id, { const res = await this.API.viewV2.fetch(datasource.id, {

View File

@ -1,6 +1,6 @@
export { createAPIClient } from "./api" export { createAPIClient } from "./api"
export type { APIClient } from "./api" export type { APIClient } from "./api"
export { fetchData, DataFetchMap } from "./fetch" export { fetchData, DataFetchMap, type DataFetchType } from "./fetch"
export * as Constants from "./constants" export * as Constants from "./constants"
export * from "./stores" export * from "./stores"
export * from "./utils" export * from "./utils"

View File

@ -0,0 +1,7 @@
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"
const config = {
preprocess: vitePreprocess(),
}
export default config

@ -1 +1 @@
Subproject commit b28dbd549284cf450be7f25ad85aadf614d08f0b Subproject commit 2dd06c2fcb3cf10d5f16f5d8fe6cd344c8e905a5

View File

@ -1,9 +1,15 @@
import * as triggers from "../../automations/triggers" import * as triggers from "../../automations/triggers"
import { sdk as coreSdk } from "@budibase/shared-core" import { sdk as coreSdk } from "@budibase/shared-core"
import { DocumentType } from "../../db/utils" import { DocumentType } from "../../db/utils"
import { updateTestHistory, removeDeprecated } from "../../automations/utils" import { updateTestHistory } from "../../automations/utils"
import { withTestFlag } from "../../utilities/redis" import { withTestFlag } from "../../utilities/redis"
import { context, cache, events, db as dbCore } from "@budibase/backend-core" import {
context,
cache,
events,
db as dbCore,
HTTPError,
} from "@budibase/backend-core"
import { automations, features } from "@budibase/pro" import { automations, features } from "@budibase/pro"
import { import {
App, App,
@ -28,20 +34,13 @@ import {
TriggerAutomationResponse, TriggerAutomationResponse,
TestAutomationRequest, TestAutomationRequest,
TestAutomationResponse, TestAutomationResponse,
Table,
} from "@budibase/types" } from "@budibase/types"
import { getActionDefinitions as actionDefs } from "../../automations/actions" import { getActionDefinitions as actionDefs } from "../../automations/actions"
import sdk from "../../sdk" import sdk from "../../sdk"
import { builderSocket } from "../../websockets" import { builderSocket } from "../../websockets"
import env from "../../environment" import env from "../../environment"
async function getActionDefinitions() {
return removeDeprecated(await actionDefs())
}
function getTriggerDefinitions() {
return removeDeprecated(triggers.TRIGGER_DEFINITIONS)
}
/************************* /*************************
* * * *
* BUILDER FUNCTIONS * * BUILDER FUNCTIONS *
@ -141,21 +140,21 @@ export async function clearLogError(
export async function getActionList( export async function getActionList(
ctx: UserCtx<void, GetAutomationActionDefinitionsResponse> ctx: UserCtx<void, GetAutomationActionDefinitionsResponse>
) { ) {
ctx.body = await getActionDefinitions() ctx.body = await actionDefs()
} }
export async function getTriggerList( export async function getTriggerList(
ctx: UserCtx<void, GetAutomationTriggerDefinitionsResponse> ctx: UserCtx<void, GetAutomationTriggerDefinitionsResponse>
) { ) {
ctx.body = getTriggerDefinitions() ctx.body = triggers.TRIGGER_DEFINITIONS
} }
export async function getDefinitionList( export async function getDefinitionList(
ctx: UserCtx<void, GetAutomationStepDefinitionsResponse> ctx: UserCtx<void, GetAutomationStepDefinitionsResponse>
) { ) {
ctx.body = { ctx.body = {
trigger: getTriggerDefinitions(), trigger: triggers.TRIGGER_DEFINITIONS,
action: await getActionDefinitions(), action: await actionDefs(),
} }
} }
@ -239,14 +238,22 @@ export async function test(
const { request, appId } = ctx const { request, appId } = ctx
const { body } = request const { body } = request
let table: Table | undefined
if (coreSdk.automations.isRowAction(automation) && body.row?.tableId) {
table = await sdk.tables.getTable(body.row?.tableId)
if (!table) {
throw new HTTPError("Table not found", 404)
}
}
ctx.body = await withTestFlag(automation._id!, async () => { ctx.body = await withTestFlag(automation._id!, async () => {
const occurredAt = new Date().getTime() const occurredAt = new Date().getTime()
await updateTestHistory(appId, automation, { ...body, occurredAt }) await updateTestHistory(appId, automation, { ...body, occurredAt })
const input = prepareTestInput(body)
const user = sdk.users.getUserContextBindings(ctx.user) const user = sdk.users.getUserContextBindings(ctx.user)
return await triggers.externalTrigger( return await triggers.externalTrigger(
automation, automation,
{ ...prepareTestInput(body), appId, user }, { ...{ ...input, ...(table ? { table } : {}) }, appId, user },
{ getResponses: true } { getResponses: true }
) )
}) })

View File

@ -263,7 +263,6 @@ export async function search(ctx: Ctx<SearchRowRequest, SearchRowResponse>) {
limit: searchRequest.limit, limit: searchRequest.limit,
sort: searchRequest.sort ?? undefined, sort: searchRequest.sort ?? undefined,
sortOrder: searchRequest.sortOrder, sortOrder: searchRequest.sortOrder,
sortType: searchRequest.sortType ?? undefined,
countRows: searchRequest.countRows, countRows: searchRequest.countRows,
version: searchRequest.version, version: searchRequest.version,
disableEscaping: searchRequest.disableEscaping, disableEscaping: searchRequest.disableEscaping,

View File

@ -63,14 +63,12 @@ function getSortOptions(request: SearchViewRowRequest, view: ViewV2) {
return { return {
sort: request.sort, sort: request.sort,
sortOrder: request.sortOrder, sortOrder: request.sortOrder,
sortType: request.sortType ?? undefined,
} }
} }
if (view.sort) { if (view.sort) {
return { return {
sort: view.sort.field, sort: view.sort.field,
sortOrder: view.sort.order, sortOrder: view.sort.order,
sortType: view.sort.type,
} }
} }

View File

@ -11,6 +11,7 @@ import {
import { configs, context, events } from "@budibase/backend-core" import { configs, context, events } from "@budibase/backend-core"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { import {
AutomationResults,
ConfigType, ConfigType,
FieldType, FieldType,
FilterCondition, FilterCondition,
@ -19,7 +20,6 @@ import {
Table, Table,
} from "@budibase/types" } from "@budibase/types"
import { mocks } from "@budibase/backend-core/tests" import { mocks } from "@budibase/backend-core/tests"
import { removeDeprecated } from "../../../automations/utils"
import { createAutomationBuilder } from "../../../automations/tests/utilities/AutomationTestBuilder" import { createAutomationBuilder } from "../../../automations/tests/utilities/AutomationTestBuilder"
import { basicTable } from "../../../tests/utilities/structures" import { basicTable } from "../../../tests/utilities/structures"
import TestConfiguration from "../../../tests/utilities/TestConfiguration" import TestConfiguration from "../../../tests/utilities/TestConfiguration"
@ -64,15 +64,11 @@ describe("/automations", () => {
it("returns all of the definitions in one", async () => { it("returns all of the definitions in one", async () => {
const { action, trigger } = await config.api.automation.getDefinitions() const { action, trigger } = await config.api.automation.getDefinitions()
let definitionsLength = Object.keys(
removeDeprecated(BUILTIN_ACTION_DEFINITIONS)
).length
expect(Object.keys(action).length).toBeGreaterThanOrEqual( expect(Object.keys(action).length).toBeGreaterThanOrEqual(
definitionsLength Object.keys(BUILTIN_ACTION_DEFINITIONS).length
) )
expect(Object.keys(trigger).length).toEqual( expect(Object.keys(trigger).length).toEqual(
Object.keys(removeDeprecated(TRIGGER_DEFINITIONS)).length Object.keys(TRIGGER_DEFINITIONS).length
) )
}) })
}) })
@ -626,7 +622,7 @@ describe("/automations", () => {
}) })
) )
const res = await config.api.automation.test(automation._id!, { const response = await config.api.automation.test(automation._id!, {
fields: {}, fields: {},
oldRow: { oldRow: {
City: oldCity, City: oldCity,
@ -636,12 +632,14 @@ describe("/automations", () => {
}, },
}) })
if (isDidNotTriggerResponse(res)) { if (isDidNotTriggerResponse(response)) {
throw new Error("Automation did not trigger") throw new Error("Automation did not trigger")
} }
const results: AutomationResults = response as AutomationResults
const expectedResult = oldCity === newCity const expectedResult = oldCity === newCity
expect(res.steps[1].outputs.result).toEqual(expectedResult) expect(results.steps[1].outputs.result).toEqual(expectedResult)
} }
) )
}) })
@ -728,7 +726,8 @@ describe("/automations", () => {
if (isDidNotTriggerResponse(res)) { if (isDidNotTriggerResponse(res)) {
expect(expectToRun).toEqual(false) expect(expectToRun).toEqual(false)
} else { } else {
expect(res.steps[1].outputs.success).toEqual(expectToRun) const results: AutomationResults = res as AutomationResults
expect(results.steps[1].outputs.success).toEqual(expectToRun)
} }
} }
) )

View File

@ -38,7 +38,7 @@ import {
import _ from "lodash" import _ from "lodash"
import tk from "timekeeper" import tk from "timekeeper"
import { encodeJSBinding } from "@budibase/string-templates" import { encodeJSBinding } from "@budibase/string-templates"
import { dataFilters } from "@budibase/shared-core" import { dataFilters, InMemorySearchQuery } from "@budibase/shared-core"
import { Knex } from "knex" import { Knex } from "knex"
import { generator, structures, mocks } from "@budibase/backend-core/tests" import { generator, structures, mocks } from "@budibase/backend-core/tests"
import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default" import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default"
@ -200,31 +200,26 @@ if (descriptions.length) {
const isView = sourceType === "view" const isView = sourceType === "view"
class SearchAssertion { class SearchAssertion {
constructor(private readonly query: SearchRowRequest) {} constructor(
private readonly query: SearchRowRequest & {
sortType?: SortType
}
) {}
private async performSearch(): Promise<SearchResponse<Row>> { private async performSearch(): Promise<SearchResponse<Row>> {
if (isInMemory) { if (isInMemory) {
const inMemoryQuery: RequiredKeys< const inMemoryQuery: RequiredKeys<InMemorySearchQuery> = {
Omit<RowSearchParams, "tableId">
> = {
sort: this.query.sort ?? undefined, sort: this.query.sort ?? undefined,
query: { ...this.query.query }, query: { ...this.query.query },
paginate: this.query.paginate,
bookmark: this.query.bookmark ?? undefined,
limit: this.query.limit, limit: this.query.limit,
sortOrder: this.query.sortOrder, sortOrder: this.query.sortOrder,
sortType: this.query.sortType ?? undefined, sortType: this.query.sortType ?? undefined,
version: this.query.version,
disableEscaping: this.query.disableEscaping,
countRows: this.query.countRows, countRows: this.query.countRows,
viewId: undefined,
fields: undefined,
indexer: undefined,
rows: undefined,
} }
return dataFilters.search(_.cloneDeep(rows), inMemoryQuery) return dataFilters.search(_.cloneDeep(rows), inMemoryQuery)
} else { } else {
return config.api.row.search(tableOrViewId, this.query) const { sortType, ...query } = this.query
return config.api.row.search(tableOrViewId, query)
} }
} }
@ -400,7 +395,9 @@ if (descriptions.length) {
} }
} }
function expectSearch(query: SearchRowRequest) { function expectSearch(
query: SearchRowRequest & { sortType?: SortType }
) {
return new SearchAssertion(query) return new SearchAssertion(query)
} }
@ -1119,6 +1116,7 @@ if (descriptions.length) {
}).toMatchExactly([{ name: "foo" }, { name: "bar" }]) }).toMatchExactly([{ name: "foo" }, { name: "bar" }])
}) })
isInMemory &&
describe("sortType STRING", () => { describe("sortType STRING", () => {
it("sorts ascending", async () => { it("sorts ascending", async () => {
await expectSearch({ await expectSearch({
@ -1319,6 +1317,7 @@ if (descriptions.length) {
}) })
}) })
isInMemory &&
describe("sortType NUMBER", () => { describe("sortType NUMBER", () => {
it("sorts ascending", async () => { it("sorts ascending", async () => {
await expectSearch({ await expectSearch({
@ -1473,6 +1472,7 @@ if (descriptions.length) {
}).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }]) }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }])
}) })
isInMemory &&
describe("sortType STRING", () => { describe("sortType STRING", () => {
it("sorts ascending", async () => { it("sorts ascending", async () => {
await expectSearch({ await expectSearch({
@ -1639,6 +1639,7 @@ if (descriptions.length) {
]) ])
}) })
isInMemory &&
describe("sortType STRING", () => { describe("sortType STRING", () => {
it("sorts ascending", async () => { it("sorts ascending", async () => {
await expectSearch({ await expectSearch({
@ -1675,6 +1676,7 @@ if (descriptions.length) {
}) })
}) })
!isInMemory &&
describe("datetime - date only", () => { describe("datetime - date only", () => {
describe.each([true, false])( describe.each([true, false])(
"saved with timestamp: %s", "saved with timestamp: %s",
@ -1847,6 +1849,7 @@ if (descriptions.length) {
]) ])
}) })
isInMemory &&
describe("sortType STRING", () => { describe("sortType STRING", () => {
it("sorts ascending", async () => { it("sorts ascending", async () => {
await expectSearch({ await expectSearch({

View File

@ -24,7 +24,6 @@ import {
SearchResponse, SearchResponse,
SearchViewRowRequest, SearchViewRowRequest,
SortOrder, SortOrder,
SortType,
StaticQuotaName, StaticQuotaName,
Table, Table,
TableSchema, TableSchema,
@ -154,7 +153,6 @@ if (descriptions.length) {
sort: { sort: {
field: "fieldToSort", field: "fieldToSort",
order: SortOrder.DESCENDING, order: SortOrder.DESCENDING,
type: SortType.STRING,
}, },
schema: { schema: {
id: { visible: true }, id: { visible: true },
@ -217,7 +215,6 @@ if (descriptions.length) {
sort: { sort: {
field: "fieldToSort", field: "fieldToSort",
order: SortOrder.DESCENDING, order: SortOrder.DESCENDING,
type: SortType.STRING,
}, },
schema: { schema: {
id: { visible: true }, id: { visible: true },
@ -1147,7 +1144,6 @@ if (descriptions.length) {
sort: { sort: {
field: generator.word(), field: generator.word(),
order: SortOrder.DESCENDING, order: SortOrder.DESCENDING,
type: SortType.STRING,
}, },
schema: { schema: {
id: { visible: true }, id: { visible: true },
@ -3153,7 +3149,6 @@ if (descriptions.length) {
{ {
field: string field: string
order?: SortOrder order?: SortOrder
type?: SortType
}, },
string[] string[]
][] = [ ][] = [
@ -3161,7 +3156,6 @@ if (descriptions.length) {
{ {
field: "name", field: "name",
order: SortOrder.ASCENDING, order: SortOrder.ASCENDING,
type: SortType.STRING,
}, },
["Alice", "Bob", "Charly", "Danny"], ["Alice", "Bob", "Charly", "Danny"],
], ],
@ -3178,22 +3172,6 @@ if (descriptions.length) {
}, },
["Danny", "Charly", "Bob", "Alice"], ["Danny", "Charly", "Bob", "Alice"],
], ],
[
{
field: "name",
order: SortOrder.DESCENDING,
type: SortType.STRING,
},
["Danny", "Charly", "Bob", "Alice"],
],
[
{
field: "age",
order: SortOrder.ASCENDING,
type: SortType.NUMBER,
},
["Danny", "Alice", "Charly", "Bob"],
],
[ [
{ {
field: "age", field: "age",
@ -3204,15 +3182,13 @@ if (descriptions.length) {
[ [
{ {
field: "age", field: "age",
order: SortOrder.DESCENDING,
}, },
["Bob", "Charly", "Alice", "Danny"], ["Danny", "Alice", "Charly", "Bob"],
], ],
[ [
{ {
field: "age", field: "age",
order: SortOrder.DESCENDING, order: SortOrder.DESCENDING,
type: SortType.NUMBER,
}, },
["Bob", "Charly", "Alice", "Danny"], ["Bob", "Charly", "Alice", "Danny"],
], ],
@ -3299,7 +3275,6 @@ if (descriptions.length) {
sort: { sort: {
field: "name", field: "name",
order: SortOrder.ASCENDING, order: SortOrder.ASCENDING,
type: SortType.STRING,
}, },
schema: viewSchema, schema: viewSchema,
}) })
@ -3307,7 +3282,6 @@ if (descriptions.length) {
const response = await config.api.viewV2.search(view.id, { const response = await config.api.viewV2.search(view.id, {
sort: sortParams.field, sort: sortParams.field,
sortOrder: sortParams.order, sortOrder: sortParams.order,
sortType: sortParams.type,
query: {}, query: {},
}) })

View File

@ -4,6 +4,7 @@ import * as createRow from "./steps/createRow"
import * as updateRow from "./steps/updateRow" import * as updateRow from "./steps/updateRow"
import * as deleteRow from "./steps/deleteRow" import * as deleteRow from "./steps/deleteRow"
import * as executeScript from "./steps/executeScript" import * as executeScript from "./steps/executeScript"
import * as executeScriptV2 from "./steps/executeScriptV2"
import * as executeQuery from "./steps/executeQuery" import * as executeQuery from "./steps/executeQuery"
import * as outgoingWebhook from "./steps/outgoingWebhook" import * as outgoingWebhook from "./steps/outgoingWebhook"
import * as serverLog from "./steps/serverLog" import * as serverLog from "./steps/serverLog"
@ -44,6 +45,7 @@ const ACTION_IMPLS: ActionImplType = {
DELETE_ROW: deleteRow.run, DELETE_ROW: deleteRow.run,
OUTGOING_WEBHOOK: outgoingWebhook.run, OUTGOING_WEBHOOK: outgoingWebhook.run,
EXECUTE_SCRIPT: executeScript.run, EXECUTE_SCRIPT: executeScript.run,
EXECUTE_SCRIPT_V2: executeScriptV2.run,
EXECUTE_QUERY: executeQuery.run, EXECUTE_QUERY: executeQuery.run,
SERVER_LOG: serverLog.run, SERVER_LOG: serverLog.run,
DELAY: delay.run, DELAY: delay.run,
@ -70,6 +72,7 @@ export const BUILTIN_ACTION_DEFINITIONS: Record<
DELETE_ROW: automations.steps.deleteRow.definition, DELETE_ROW: automations.steps.deleteRow.definition,
OUTGOING_WEBHOOK: automations.steps.outgoingWebhook.definition, OUTGOING_WEBHOOK: automations.steps.outgoingWebhook.definition,
EXECUTE_SCRIPT: automations.steps.executeScript.definition, EXECUTE_SCRIPT: automations.steps.executeScript.definition,
EXECUTE_SCRIPT_V2: automations.steps.executeScriptV2.definition,
EXECUTE_QUERY: automations.steps.executeQuery.definition, EXECUTE_QUERY: automations.steps.executeQuery.definition,
SERVER_LOG: automations.steps.serverLog.definition, SERVER_LOG: automations.steps.serverLog.definition,
DELAY: automations.steps.delay.definition, DELAY: automations.steps.delay.definition,

View File

@ -0,0 +1,48 @@
import * as automationUtils from "../automationUtils"
import {
ExecuteScriptStepInputs,
ExecuteScriptStepOutputs,
} from "@budibase/types"
import { processStringSync } from "@budibase/string-templates"
export async function run({
inputs,
context,
}: {
inputs: ExecuteScriptStepInputs
context: Record<string, any>
}): Promise<ExecuteScriptStepOutputs> {
let { code } = inputs
if (code == null) {
return {
success: false,
response: {
message: "Invalid inputs",
},
}
}
code = code.trim()
if (!code.startsWith("{{ js ")) {
return {
success: false,
response: {
message: "Expected code to be a {{ js }} template block",
},
}
}
try {
return {
success: true,
value: processStringSync(inputs.code, context, { noThrow: false }),
}
} catch (err) {
return {
success: false,
response: automationUtils.getError(err),
}
}
}

View File

@ -0,0 +1,158 @@
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
import * as automation from "../index"
import * as setup from "./utilities"
import { Table } from "@budibase/types"
function encodeJS(js: string): string {
return `{{ js "${Buffer.from(js, "utf-8").toString("base64")}" }}`
}
describe("Execute Script Automations", () => {
let config = setup.getConfig(),
table: Table
beforeEach(async () => {
await automation.init()
await config.init()
table = await config.createTable()
await config.createRow()
})
afterAll(setup.afterAll)
it("should execute a basic script and return the result", async () => {
config.name = "Basic Script Execution"
const builder = createAutomationBuilder(config)
const results = await builder
.onAppAction()
.executeScriptV2({ code: encodeJS("return 2 + 2") })
.test({ fields: {} })
expect(results.steps[0].outputs.value).toEqual(4)
})
it("should access bindings from previous steps", async () => {
config.name = "Access Bindings"
const builder = createAutomationBuilder(config)
const results = await builder
.onAppAction()
.executeScriptV2(
{
code: encodeJS(`return $("trigger.fields.data").map(x => x * 2)`),
},
{ stepId: "binding-script-step" }
)
.test({ fields: { data: [1, 2, 3] } })
expect(results.steps[0].outputs.value).toEqual([2, 4, 6])
})
it("should handle script execution errors gracefully", async () => {
config.name = "Handle Script Errors"
const builder = createAutomationBuilder(config)
const results = await builder
.onAppAction()
.executeScriptV2({
code: encodeJS("return nonexistentVariable.map(x => x)"),
})
.test({ fields: {} })
expect(results.steps[0].outputs.response).toContain(
"ReferenceError: nonexistentVariable is not defined"
)
expect(results.steps[0].outputs.success).toEqual(false)
})
it("should handle conditional logic in scripts", async () => {
config.name = "Conditional Script Logic"
const builder = createAutomationBuilder(config)
const results = await builder
.onAppAction()
.executeScriptV2({
code: encodeJS(`
if ($("trigger.fields.value") > 5) {
return "Value is greater than 5";
} else {
return "Value is 5 or less";
}
`),
})
.test({ fields: { value: 10 } })
expect(results.steps[0].outputs.value).toEqual("Value is greater than 5")
})
it("should use multiple steps and validate script execution", async () => {
config.name = "Multi-Step Script Execution"
const builder = createAutomationBuilder(config)
const results = await builder
.onAppAction()
.serverLog(
{ text: "Starting multi-step automation" },
{ stepId: "start-log-step" }
)
.createRow(
{ row: { name: "Test Row", value: 42, tableId: table._id } },
{ stepId: "abc123" }
)
.executeScriptV2(
{
code: encodeJS(`
const createdRow = $("steps")['abc123'];
return createdRow.row.value * 2;
`),
},
{ stepId: "ScriptingStep1" }
)
.serverLog({
text: `Final result is {{ steps.ScriptingStep1.value }}`,
})
.test({ fields: {} })
expect(results.steps[0].outputs.message).toContain(
"Starting multi-step automation"
)
expect(results.steps[1].outputs.row.value).toEqual(42)
expect(results.steps[2].outputs.value).toEqual(84)
expect(results.steps[3].outputs.message).toContain("Final result is 84")
})
it("should fail if the code has not been encoded as a handlebars template", async () => {
config.name = "Invalid Code Encoding"
const builder = createAutomationBuilder(config)
const results = await builder
.onAppAction()
.executeScriptV2({
code: "return 2 + 2",
})
.test({ fields: {} })
expect(results.steps[0].outputs.response.message).toEqual(
"Expected code to be a {{ js }} template block"
)
expect(results.steps[0].outputs.success).toEqual(false)
})
it("does not process embedded handlebars templates", async () => {
config.name = "Embedded Handlebars"
const builder = createAutomationBuilder(config)
const results = await builder
.onAppAction()
.executeScriptV2({
code: encodeJS(`return "{{ triggers.row.whatever }}"`),
})
.test({ fields: {} })
expect(results.steps[0].outputs.value).toEqual(
"{{ triggers.row.whatever }}"
)
expect(results.steps[0].outputs.success).toEqual(true)
})
})

View File

@ -12,7 +12,7 @@ describe("Webhook trigger test", () => {
async function createWebhookAutomation() { async function createWebhookAutomation() {
const { automation } = await createAutomationBuilder(config) const { automation } = await createAutomationBuilder(config)
.onWebhook({ fields: { parameter: "string" } }) .onWebhook({ body: { parameter: "string" } })
.createRow({ .createRow({
row: { tableId: table._id!, name: "{{ trigger.parameter }}" }, row: { tableId: table._id!, name: "{{ trigger.parameter }}" },
}) })

View File

@ -4,6 +4,7 @@ import { TRIGGER_DEFINITIONS } from "../../triggers"
import { import {
Automation, Automation,
AutomationActionStepId, AutomationActionStepId,
AutomationResults,
AutomationStep, AutomationStep,
AutomationStepInputs, AutomationStepInputs,
AutomationTrigger, AutomationTrigger,
@ -100,6 +101,7 @@ class BranchStepBuilder<TStep extends AutomationTriggerStepId> {
loop = this.step(AutomationActionStepId.LOOP) loop = this.step(AutomationActionStepId.LOOP)
serverLog = this.step(AutomationActionStepId.SERVER_LOG) serverLog = this.step(AutomationActionStepId.SERVER_LOG)
executeScript = this.step(AutomationActionStepId.EXECUTE_SCRIPT) executeScript = this.step(AutomationActionStepId.EXECUTE_SCRIPT)
executeScriptV2 = this.step(AutomationActionStepId.EXECUTE_SCRIPT_V2)
filter = this.step(AutomationActionStepId.FILTER) filter = this.step(AutomationActionStepId.FILTER)
bash = this.step(AutomationActionStepId.EXECUTE_BASH) bash = this.step(AutomationActionStepId.EXECUTE_BASH)
openai = this.step(AutomationActionStepId.OPENAI) openai = this.step(AutomationActionStepId.OPENAI)
@ -212,10 +214,11 @@ class AutomationRunner<TStep extends AutomationTriggerStepId> {
throw new Error(response.message) throw new Error(response.message)
} }
const results: AutomationResults = response as AutomationResults
// Remove the trigger step from the response. // Remove the trigger step from the response.
response.steps.shift() results.steps.shift()
return response return results
} }
async trigger( async trigger(

View File

@ -22,6 +22,7 @@ import {
UserBindings, UserBindings,
AutomationResults, AutomationResults,
DidNotTriggerResponse, DidNotTriggerResponse,
Table,
} from "@budibase/types" } from "@budibase/types"
import { executeInThread } from "../threads/automation" import { executeInThread } from "../threads/automation"
import { dataFilters, sdk } from "@budibase/shared-core" import { dataFilters, sdk } from "@budibase/shared-core"
@ -154,6 +155,7 @@ interface AutomationTriggerParams {
timeout?: number timeout?: number
appId?: string appId?: string
user?: UserBindings user?: UserBindings
table?: Table
} }
export async function externalTrigger( export async function externalTrigger(

View File

@ -4,17 +4,8 @@ import { automationQueue } from "./bullboard"
import { updateEntityMetadata } from "../utilities" import { updateEntityMetadata } from "../utilities"
import { context, db as dbCore, utils } from "@budibase/backend-core" import { context, db as dbCore, utils } from "@budibase/backend-core"
import { getAutomationMetadataParams } from "../db/utils" import { getAutomationMetadataParams } from "../db/utils"
import { cloneDeep } from "lodash/fp"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { import { Automation, AutomationJob, MetadataType } from "@budibase/types"
Automation,
AutomationActionStepId,
AutomationJob,
AutomationStepDefinition,
AutomationTriggerDefinition,
AutomationTriggerStepId,
MetadataType,
} from "@budibase/types"
import { automationsEnabled } from "../features" import { automationsEnabled } from "../features"
import { helpers, REBOOT_CRON } from "@budibase/shared-core" import { helpers, REBOOT_CRON } from "@budibase/shared-core"
import tracer from "dd-trace" import tracer from "dd-trace"
@ -113,23 +104,6 @@ export async function updateTestHistory(
) )
} }
export function removeDeprecated<
T extends
| Record<keyof typeof AutomationTriggerStepId, AutomationTriggerDefinition>
| Record<keyof typeof AutomationActionStepId, AutomationStepDefinition>
>(definitions: T): T {
const base: Record<
string,
AutomationTriggerDefinition | AutomationStepDefinition
> = cloneDeep(definitions)
for (let key of Object.keys(base)) {
if (base[key].deprecated) {
delete base[key]
}
}
return base as T
}
// end the repetition and the job itself // end the repetition and the job itself
export async function disableAllCrons(appId: any) { export async function disableAllCrons(appId: any) {
const promises = [] const promises = []

View File

@ -168,6 +168,7 @@ class S3Integration implements IntegrationBase {
secretAccessKey: config.secretAccessKey, secretAccessKey: config.secretAccessKey,
}, },
region: config.region, region: config.region,
endpoint: config.endpoint,
} }
if (config.endpoint) { if (config.endpoint) {
this.config.forcePathStyle = true this.config.forcePathStyle = true

View File

@ -34,7 +34,7 @@ const checkAuthorized = async (
const isCreatorApi = permType === PermissionType.CREATOR const isCreatorApi = permType === PermissionType.CREATOR
const isBuilderApi = permType === PermissionType.BUILDER const isBuilderApi = permType === PermissionType.BUILDER
const isGlobalBuilder = users.isGlobalBuilder(ctx.user) const isGlobalBuilder = users.isGlobalBuilder(ctx.user)
const isCreator = await users.isCreator(ctx.user) const isCreator = await users.isCreatorAsync(ctx.user)
const isBuilder = appId const isBuilder = appId
? users.isBuilder(ctx.user, appId) ? users.isBuilder(ctx.user, appId)
: users.hasBuilderPermissions(ctx.user) : users.hasBuilderPermissions(ctx.user)

View File

@ -46,7 +46,6 @@ export async function search(
query: options.query, query: options.query,
sort: options.sort, sort: options.sort,
sortOrder: options.sortOrder, sortOrder: options.sortOrder,
sortType: options.sortType,
limit: options.limit, limit: options.limit,
bookmark: options.bookmark, bookmark: options.bookmark,
paginate: options.paginate, paginate: options.paginate,

View File

@ -1,5 +1,6 @@
import { import {
Aggregation, Aggregation,
AutoFieldSubType,
CalculationType, CalculationType,
DocumentType, DocumentType,
EnrichedQueryJson, EnrichedQueryJson,
@ -420,7 +421,11 @@ export async function search(
} }
} else if (sortField) { } else if (sortField) {
const sortType = const sortType =
sortField.type === FieldType.NUMBER ? SortType.NUMBER : SortType.STRING sortField.type === FieldType.NUMBER ||
(sortField.type === FieldType.AUTO &&
sortField.subtype === AutoFieldSubType.AUTO_ID)
? SortType.NUMBER
: SortType.STRING
request.sort = { request.sort = {
[mapToUserColumn(sortField.name)]: { [mapToUserColumn(sortField.name)]: {
direction: params.sortOrder || SortOrder.ASCENDING, direction: params.sortOrder || SortOrder.ASCENDING,

View File

@ -99,6 +99,7 @@ export default class TestConfiguration {
request?: supertest.SuperTest<supertest.Test> request?: supertest.SuperTest<supertest.Test>
started: boolean started: boolean
appId?: string appId?: string
name?: string
allApps: App[] allApps: App[]
app?: App app?: App
prodApp?: App prodApp?: App

View File

@ -555,8 +555,16 @@ class Orchestrator {
throw new Error(`Cannot find automation step by name ${step.stepId}`) throw new Error(`Cannot find automation step by name ${step.stepId}`)
} }
const inputs = automationUtils.cleanInputValues( let inputs = cloneDeep(step.inputs)
await processObject(cloneDeep(step.inputs), ctx), if (step.stepId !== AutomationActionStepId.EXECUTE_SCRIPT_V2) {
// The EXECUTE_SCRIPT_V2 step saves its input.code value as a `{{ js
// "..." }}` template, and expects to receive it that way in the
// function that runs it. So we skip this next bit for that step.
inputs = await processObject(inputs, ctx)
}
inputs = automationUtils.cleanInputValues(
inputs,
step.schema.inputs.properties step.schema.inputs.properties
) )

View File

@ -10,6 +10,7 @@ import {
export const definition: AutomationStepDefinition = { export const definition: AutomationStepDefinition = {
name: "JS Scripting", name: "JS Scripting",
tagline: "Execute JavaScript Code", tagline: "Execute JavaScript Code",
deprecated: true,
icon: "Code", icon: "Code",
description: "Run a piece of JavaScript code in your automation", description: "Run a piece of JavaScript code in your automation",
type: AutomationStepType.ACTION, type: AutomationStepType.ACTION,

View File

@ -0,0 +1,48 @@
import {
AutomationActionStepId,
AutomationCustomIOType,
AutomationFeature,
AutomationIOType,
AutomationStepDefinition,
AutomationStepType,
} from "@budibase/types"
export const definition: AutomationStepDefinition = {
name: "JavaScript",
tagline: "Execute JavaScript Code",
icon: "Brackets",
description: "Run a piece of JavaScript code in your automation",
type: AutomationStepType.ACTION,
internal: true,
new: true,
stepId: AutomationActionStepId.EXECUTE_SCRIPT_V2,
inputs: {},
features: {
[AutomationFeature.LOOPING]: true,
},
schema: {
inputs: {
properties: {
code: {
type: AutomationIOType.STRING,
customType: AutomationCustomIOType.CODE,
title: "Code",
},
},
required: ["code"],
},
outputs: {
properties: {
value: {
type: AutomationIOType.STRING,
description: "The result of the return statement",
},
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
},
required: ["success"],
},
},
}

View File

@ -7,6 +7,7 @@ export * as deleteRow from "./deleteRow"
export * as discord from "./discord" export * as discord from "./discord"
export * as executeQuery from "./executeQuery" export * as executeQuery from "./executeQuery"
export * as executeScript from "./executeScript" export * as executeScript from "./executeScript"
export * as executeScriptV2 from "./executeScriptV2"
export * as filter from "./filter" export * as filter from "./filter"
export * as loop from "./loop" export * as loop from "./loop"
export * as make from "./make" export * as make from "./make"

View File

@ -11,7 +11,6 @@ import {
SortType, SortType,
FieldConstraints, FieldConstraints,
SortOrder, SortOrder,
RowSearchParams,
EmptyFilterOption, EmptyFilterOption,
SearchResponse, SearchResponse,
Table, Table,
@ -25,6 +24,8 @@ import {
isArraySearchOperator, isArraySearchOperator,
isRangeSearchOperator, isRangeSearchOperator,
SearchFilter, SearchFilter,
WithRequired,
SearchParams,
} from "@budibase/types" } from "@budibase/types"
import dayjs from "dayjs" import dayjs from "dayjs"
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants" import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
@ -521,9 +522,19 @@ export function fixupFilterArrays(filters: SearchFilters) {
return filters return filters
} }
type SearchQuery = WithRequired<
Pick<
SearchParams,
"query" | "sort" | "sortOrder" | "sortType" | "limit" | "countRows"
>,
"query"
>
export type InMemorySearchQuery = SearchQuery
export function search<T extends Record<string, any>>( export function search<T extends Record<string, any>>(
docs: T[], docs: T[],
query: Omit<RowSearchParams, "tableId"> query: SearchQuery
): SearchResponse<T> { ): SearchResponse<T> {
let result = runQuery(docs, query.query) let result = runQuery(docs, query.query)
if (query.sort) { if (query.sort) {

View File

@ -1,5 +1,6 @@
export * from "./constants" export * from "./constants"
export * as dataFilters from "./filters" export * as dataFilters from "./filters"
export type * from "./filters"
export * as helpers from "./helpers" export * as helpers from "./helpers"
export * as utils from "./utils" export * as utils from "./utils"
export * as sdk from "./sdk" export * as sdk from "./sdk"

View File

@ -1,3 +1,4 @@
import { AutomationJob } from "../../../sdk/automations"
import { import {
Automation, Automation,
AutomationActionStepId, AutomationActionStepId,
@ -78,10 +79,25 @@ export interface TestAutomationRequest {
row?: Row row?: Row
oldRow?: Row oldRow?: Row
} }
export type TestAutomationResponse = AutomationResults | DidNotTriggerResponse
export function isDidNotTriggerResponse( export function isDidNotTriggerResponse(
response: TestAutomationResponse response: TestAutomationResponse
): response is DidNotTriggerResponse { ): response is DidNotTriggerResponse {
return !!("message" in response && response.message) return !!("message" in response && response.message)
} }
export function isAutomationResults(
response: TestAutomationResponse
): response is AutomationResults {
return !!(
"steps" in response &&
response.steps &&
"trigger" in response &&
response.trigger
)
}
export type TestAutomationResponse =
| AutomationResults
| DidNotTriggerResponse
| AutomationJob

View File

@ -11,7 +11,7 @@ export interface SaveQueryRequest extends Query {}
export interface SaveQueryResponse extends Query {} export interface SaveQueryResponse extends Query {}
export interface ImportRestQueryRequest { export interface ImportRestQueryRequest {
datasourceId: string datasourceId?: string
data: string data: string
datasource: Datasource datasource: Datasource
} }

View File

@ -8,11 +8,7 @@ import {
SearchFilterKey, SearchFilterKey,
} from "../../../../sdk" } from "../../../../sdk"
import { Row } from "../../../../documents" import { Row } from "../../../../documents"
import { import { PaginationResponse, SortOrder } from "../../../../api/web/pagination"
PaginationResponse,
SortOrder,
SortType,
} from "../../../../api/web/pagination"
import { z } from "zod" import { z } from "zod"
const fieldKey = z const fieldKey = z
@ -70,7 +66,6 @@ const searchRowRequest = z.object({
limit: z.number().optional(), limit: z.number().optional(),
sort: z.string().nullish(), sort: z.string().nullish(),
sortOrder: z.nativeEnum(SortOrder).optional(), sortOrder: z.nativeEnum(SortOrder).optional(),
sortType: z.nativeEnum(SortType).nullish(),
version: z.string().optional(), version: z.string().optional(),
disableEscaping: z.boolean().optional(), disableEscaping: z.boolean().optional(),
countRows: z.boolean().optional(), countRows: z.boolean().optional(),
@ -83,7 +78,6 @@ export type SearchViewRowRequest = Pick<
SearchRowRequest, SearchRowRequest,
| "sort" | "sort"
| "sortOrder" | "sortOrder"
| "sortType"
| "limit" | "limit"
| "bookmark" | "bookmark"
| "paginate" | "paginate"

View File

@ -1,3 +1,4 @@
import { Table } from "@budibase/types"
import { SortOrder } from "../../../api" import { SortOrder } from "../../../api"
import { import {
SearchFilters, SearchFilters,
@ -305,6 +306,7 @@ export type RowUpdatedTriggerOutputs = {
row: Row row: Row
id: string id: string
revision?: string revision?: string
oldRow?: Row
} }
export type WebhookTriggerInputs = { export type WebhookTriggerInputs = {
@ -312,6 +314,17 @@ export type WebhookTriggerInputs = {
triggerUrl: string triggerUrl: string
} }
export type WebhookTriggerOutputs = { export type WebhookTriggerOutputs = Record<string, any> & {
fields: Record<string, any> body: Record<string, any>
}
export type RowActionTriggerInputs = {
tableId: string
}
export type RowActionTriggerOutputs = {
row: Row
id: string
revision?: string
table: Table
} }

View File

@ -63,6 +63,7 @@ export enum AutomationActionStepId {
EXECUTE_BASH = "EXECUTE_BASH", EXECUTE_BASH = "EXECUTE_BASH",
OUTGOING_WEBHOOK = "OUTGOING_WEBHOOK", OUTGOING_WEBHOOK = "OUTGOING_WEBHOOK",
EXECUTE_SCRIPT = "EXECUTE_SCRIPT", EXECUTE_SCRIPT = "EXECUTE_SCRIPT",
EXECUTE_SCRIPT_V2 = "EXECUTE_SCRIPT_V2",
EXECUTE_QUERY = "EXECUTE_QUERY", EXECUTE_QUERY = "EXECUTE_QUERY",
SERVER_LOG = "SERVER_LOG", SERVER_LOG = "SERVER_LOG",
DELAY = "DELAY", DELAY = "DELAY",
@ -135,15 +136,7 @@ export interface Automation extends Document {
internal?: boolean internal?: boolean
type?: string type?: string
disabled?: boolean disabled?: boolean
testData?: { testData?: AutomationTriggerResultOutputs
row?: Row
meta: {
[key: string]: unknown
}
id: string
revision: string
oldRow?: Row
}
} }
export interface BaseIOStructure { export interface BaseIOStructure {

View File

@ -84,6 +84,10 @@ export type ActionImplementations<T extends Hosting> = {
ExecuteScriptStepInputs, ExecuteScriptStepInputs,
ExecuteScriptStepOutputs ExecuteScriptStepOutputs
> >
[AutomationActionStepId.EXECUTE_SCRIPT_V2]: ActionImplementation<
ExecuteScriptStepInputs,
ExecuteScriptStepOutputs
>
[AutomationActionStepId.FILTER]: ActionImplementation< [AutomationActionStepId.FILTER]: ActionImplementation<
FilterStepInputs, FilterStepInputs,
FilterStepOutputs FilterStepOutputs
@ -155,6 +159,7 @@ export interface AutomationStepSchemaBase {
type: AutomationStepType type: AutomationStepType
internal?: boolean internal?: boolean
deprecated?: boolean deprecated?: boolean
new?: boolean
blockToLoop?: string blockToLoop?: string
schema: { schema: {
inputs: InputOutputBlock inputs: InputOutputBlock
@ -177,6 +182,8 @@ export type AutomationStepInputs<T extends AutomationActionStepId> =
? ExecuteQueryStepInputs ? ExecuteQueryStepInputs
: T extends AutomationActionStepId.EXECUTE_SCRIPT : T extends AutomationActionStepId.EXECUTE_SCRIPT
? ExecuteScriptStepInputs ? ExecuteScriptStepInputs
: T extends AutomationActionStepId.EXECUTE_SCRIPT_V2
? ExecuteScriptStepInputs
: T extends AutomationActionStepId.FILTER : T extends AutomationActionStepId.FILTER
? FilterStepInputs ? FilterStepInputs
: T extends AutomationActionStepId.QUERY_ROWS : T extends AutomationActionStepId.QUERY_ROWS
@ -279,6 +286,9 @@ export type ExecuteQueryStep =
export type ExecuteScriptStep = export type ExecuteScriptStep =
AutomationStepSchema<AutomationActionStepId.EXECUTE_SCRIPT> AutomationStepSchema<AutomationActionStepId.EXECUTE_SCRIPT>
export type ExecuteScriptV2Step =
AutomationStepSchema<AutomationActionStepId.EXECUTE_SCRIPT_V2>
export type FilterStep = AutomationStepSchema<AutomationActionStepId.FILTER> export type FilterStep = AutomationStepSchema<AutomationActionStepId.FILTER>
export type QueryRowsStep = export type QueryRowsStep =
@ -325,6 +335,7 @@ export type AutomationStep =
| DeleteRowStep | DeleteRowStep
| ExecuteQueryStep | ExecuteQueryStep
| ExecuteScriptStep | ExecuteScriptStep
| ExecuteScriptV2Step
| FilterStep | FilterStep
| QueryRowsStep | QueryRowsStep
| SendEmailSmtpStep | SendEmailSmtpStep

View File

@ -136,7 +136,7 @@ export interface Database {
get<T extends Document>(id?: string): Promise<T> get<T extends Document>(id?: string): Promise<T>
tryGet<T extends Document>(id?: string): Promise<T | undefined> tryGet<T extends Document>(id?: string): Promise<T | undefined>
getMultiple<T extends Document>( getMultiple<T extends Document>(
ids: string[], ids?: string[],
opts?: { allowMissing?: boolean; excludeDocs?: boolean } opts?: { allowMissing?: boolean; excludeDocs?: boolean }
): Promise<T[]> ): Promise<T[]>
remove(idOrDoc: Document): Promise<Nano.DocumentDestroyResponse> remove(idOrDoc: Document): Promise<Nano.DocumentDestroyResponse>

View File

@ -50,7 +50,7 @@ export interface SearchParams {
// when searching for rows we want a more extensive search type that requires certain properties // when searching for rows we want a more extensive search type that requires certain properties
export interface RowSearchParams export interface RowSearchParams
extends WithRequired<SearchParams, "tableId" | "query"> {} extends WithRequired<Omit<SearchParams, "sortType">, "tableId" | "query"> {}
export interface SearchResponse<T> { export interface SearchResponse<T> {
rows: T[] rows: T[]

View File

@ -1,3 +1,8 @@
import {
GetAutomationActionDefinitionsResponse,
GetAutomationTriggerDefinitionsResponse,
} from "../../api"
export interface BranchPath { export interface BranchPath {
stepIdx: number stepIdx: number
branchIdx: number branchIdx: number
@ -6,7 +11,7 @@ export interface BranchPath {
} }
export interface BlockDefinitions { export interface BlockDefinitions {
TRIGGER: Record<string, any> TRIGGER: Partial<GetAutomationTriggerDefinitionsResponse>
CREATABLE_TRIGGER: Record<string, any> CREATABLE_TRIGGER: Partial<GetAutomationTriggerDefinitionsResponse>
ACTION: Record<string, any> ACTION: Partial<GetAutomationActionDefinitionsResponse>
} }

View File

@ -20499,10 +20499,10 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
svelte-check@^4.1.0: svelte-check@^4.1.5:
version "4.1.0" version "4.1.5"
resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-4.1.0.tgz#4389c1c88aa24f3d06fe0df94f9075a55017256d" resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-4.1.5.tgz#afdb3f8050c123064124d5aa7821365c7befa7a4"
integrity sha512-AflEZYqI578KuDZcpcorPSf597LStxlkN7XqXi38u09zlHODVKd7c+7OuubGzbhgGRUqNTdQCZ+Ga96iRXEf2g== integrity sha512-Gb0T2IqBNe1tLB9EB1Qh+LOe+JB8wt2/rNBDGvkxQVvk8vNeAoG+vZgFB/3P5+zC7RWlyBlzm9dVjZFph/maIg==
dependencies: dependencies:
"@jridgewell/trace-mapping" "^0.3.25" "@jridgewell/trace-mapping" "^0.3.25"
chokidar "^4.0.1" chokidar "^4.0.1"