Merge branch 'master' into fix/couchdb-integration
This commit is contained in:
commit
4933658b98
|
@ -214,6 +214,7 @@ jobs:
|
||||||
echo "pro_commit=$pro_commit"
|
echo "pro_commit=$pro_commit"
|
||||||
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
|
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
|
||||||
echo "base_commit=$base_commit"
|
echo "base_commit=$base_commit"
|
||||||
|
echo "base_commit=$base_commit" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
base_commit_excluding_merges=$(git log --no-merges -n 1 --format=format:%H $base_commit)
|
base_commit_excluding_merges=$(git log --no-merges -n 1 --format=format:%H $base_commit)
|
||||||
echo "base_commit_excluding_merges=$base_commit_excluding_merges"
|
echo "base_commit_excluding_merges=$base_commit_excluding_merges"
|
||||||
|
@ -230,7 +231,7 @@ jobs:
|
||||||
base_commit_excluding_merges='${{ steps.get_pro_commits.outputs.base_commit_excluding_merges }}'
|
base_commit_excluding_merges='${{ steps.get_pro_commits.outputs.base_commit_excluding_merges }}'
|
||||||
pro_commit='${{ steps.get_pro_commits.outputs.pro_commit }}'
|
pro_commit='${{ steps.get_pro_commits.outputs.pro_commit }}'
|
||||||
|
|
||||||
any_commit=$(git log --no-merges $base_commit...$pro_commit)
|
any_commit=$(git log --no-merges $base_commit_excluding_merges...$pro_commit)
|
||||||
|
|
||||||
if [ -n "$any_commit" ]; then
|
if [ -n "$any_commit" ]; then
|
||||||
echo $any_commit
|
echo $any_commit
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "2.29.9",
|
"version": "2.29.11",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -200,6 +200,9 @@ const environment = {
|
||||||
},
|
},
|
||||||
ROLLING_LOG_MAX_SIZE: process.env.ROLLING_LOG_MAX_SIZE || "10M",
|
ROLLING_LOG_MAX_SIZE: process.env.ROLLING_LOG_MAX_SIZE || "10M",
|
||||||
DISABLE_SCIM_CALLS: process.env.DISABLE_SCIM_CALLS,
|
DISABLE_SCIM_CALLS: process.env.DISABLE_SCIM_CALLS,
|
||||||
|
BB_ADMIN_USER_EMAIL: process.env.BB_ADMIN_USER_EMAIL,
|
||||||
|
BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD,
|
||||||
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up any environment variable edge cases
|
// clean up any environment variable edge cases
|
||||||
|
|
|
@ -221,7 +221,7 @@ export class UserDB {
|
||||||
const tenantId = getTenantId()
|
const tenantId = getTenantId()
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
|
|
||||||
let { email, _id, userGroups = [], roles } = user
|
const { email, _id, userGroups = [], roles } = user
|
||||||
|
|
||||||
if (!email && !_id) {
|
if (!email && !_id) {
|
||||||
throw new Error("_id or email is required")
|
throw new Error("_id or email is required")
|
||||||
|
@ -231,11 +231,10 @@ export class UserDB {
|
||||||
if (_id) {
|
if (_id) {
|
||||||
// try to get existing user from db
|
// try to get existing user from db
|
||||||
try {
|
try {
|
||||||
dbUser = (await db.get(_id)) as User
|
dbUser = await usersCore.getById(_id)
|
||||||
if (email && dbUser.email !== email) {
|
if (email && dbUser.email !== email && !opts.allowChangingEmail) {
|
||||||
throw "Email address cannot be changed"
|
throw new Error("Email address cannot be changed")
|
||||||
}
|
}
|
||||||
email = dbUser.email
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.status === 404) {
|
if (e.status === 404) {
|
||||||
// do nothing, save this new user with the id specified - required for SSO auth
|
// do nothing, save this new user with the id specified - required for SSO auth
|
||||||
|
@ -271,13 +270,13 @@ export class UserDB {
|
||||||
|
|
||||||
// make sure we set the _id field for a new user
|
// make sure we set the _id field for a new user
|
||||||
// Also if this is a new user, associate groups with them
|
// Also if this is a new user, associate groups with them
|
||||||
let groupPromises = []
|
const groupPromises = []
|
||||||
if (!_id) {
|
if (!_id) {
|
||||||
_id = builtUser._id!
|
|
||||||
|
|
||||||
if (userGroups.length > 0) {
|
if (userGroups.length > 0) {
|
||||||
for (let groupId of userGroups) {
|
for (let groupId of userGroups) {
|
||||||
groupPromises.push(UserDB.groups.addUsers(groupId, [_id!]))
|
groupPromises.push(
|
||||||
|
UserDB.groups.addUsers(groupId, [builtUser._id!])
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -288,6 +287,11 @@ export class UserDB {
|
||||||
builtUser._rev = response.rev
|
builtUser._rev = response.rev
|
||||||
|
|
||||||
await eventHelpers.handleSaveEvents(builtUser, dbUser)
|
await eventHelpers.handleSaveEvents(builtUser, dbUser)
|
||||||
|
if (dbUser && builtUser.email !== dbUser.email) {
|
||||||
|
// Remove the plaform email reference if the email changed
|
||||||
|
await platform.users.removeUser({ email: dbUser.email } as User)
|
||||||
|
}
|
||||||
|
|
||||||
await platform.users.addUser(
|
await platform.users.addUser(
|
||||||
tenantId,
|
tenantId,
|
||||||
builtUser._id!,
|
builtUser._id!,
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
import { User, UserStatus } from "@budibase/types"
|
||||||
|
import { DBTestConfiguration, generator, structures } from "../../../tests"
|
||||||
|
import { UserDB } from "../db"
|
||||||
|
import { searchExistingEmails } from "../lookup"
|
||||||
|
|
||||||
|
const db = UserDB
|
||||||
|
|
||||||
|
const config = new DBTestConfiguration()
|
||||||
|
|
||||||
|
const quotas = {
|
||||||
|
addUsers: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(
|
||||||
|
(_change: number, _creatorsChange: number, cb?: () => Promise<any>) =>
|
||||||
|
cb && cb()
|
||||||
|
),
|
||||||
|
removeUsers: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(
|
||||||
|
(_change: number, _creatorsChange: number, cb?: () => Promise<any>) =>
|
||||||
|
cb && cb()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
const groups = {
|
||||||
|
addUsers: jest.fn(),
|
||||||
|
getBulk: jest.fn(),
|
||||||
|
getGroupBuilderAppIds: jest.fn(),
|
||||||
|
}
|
||||||
|
const features = { isSSOEnforced: jest.fn(), isAppBuildersEnabled: jest.fn() }
|
||||||
|
|
||||||
|
describe("UserDB", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
db.init(quotas, groups, features)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("save", () => {
|
||||||
|
describe("create", () => {
|
||||||
|
it("creating a new user will persist it", async () => {
|
||||||
|
const email = generator.email({})
|
||||||
|
const user: User = structures.users.user({
|
||||||
|
email,
|
||||||
|
tenantId: config.getTenantId(),
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const saveUserResponse = await db.save(user)
|
||||||
|
|
||||||
|
const persistedUser = await db.getUserByEmail(email)
|
||||||
|
expect(persistedUser).toEqual({
|
||||||
|
...user,
|
||||||
|
_id: saveUserResponse._id,
|
||||||
|
_rev: expect.stringMatching(/^1-\w+/),
|
||||||
|
password: expect.not.stringMatching(user.password!),
|
||||||
|
status: UserStatus.ACTIVE,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("the same email cannot be used twice in the same tenant", async () => {
|
||||||
|
const email = generator.email({})
|
||||||
|
const user: User = structures.users.user({
|
||||||
|
email,
|
||||||
|
tenantId: config.getTenantId(),
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.doInTenant(() => db.save(user))
|
||||||
|
|
||||||
|
await config.doInTenant(() =>
|
||||||
|
expect(db.save(user)).rejects.toThrow(
|
||||||
|
`Email already in use: '${email}'`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("the same email cannot be used twice in different tenants", async () => {
|
||||||
|
const email = generator.email({})
|
||||||
|
const user: User = structures.users.user({
|
||||||
|
email,
|
||||||
|
tenantId: config.getTenantId(),
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.doInTenant(() => db.save(user))
|
||||||
|
|
||||||
|
config.newTenant()
|
||||||
|
await config.doInTenant(() =>
|
||||||
|
expect(db.save(user)).rejects.toThrow(
|
||||||
|
`Email already in use: '${email}'`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("update", () => {
|
||||||
|
let user: User
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await config.doInTenant(() =>
|
||||||
|
db.save(
|
||||||
|
structures.users.user({
|
||||||
|
email: generator.email({}),
|
||||||
|
tenantId: config.getTenantId(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can update user properties", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const updatedName = generator.first()
|
||||||
|
user.firstName = updatedName
|
||||||
|
|
||||||
|
await db.save(user)
|
||||||
|
|
||||||
|
const persistedUser = await db.getUserByEmail(user.email)
|
||||||
|
expect(persistedUser).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: user._id,
|
||||||
|
email: user.email,
|
||||||
|
firstName: updatedName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("email cannot be updated by default", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
await expect(
|
||||||
|
db.save({ ...user, email: generator.email({}) })
|
||||||
|
).rejects.toThrow("Email address cannot be changed")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("email can be updated if specified", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const newEmail = generator.email({})
|
||||||
|
|
||||||
|
await db.save(
|
||||||
|
{ ...user, email: newEmail },
|
||||||
|
{ allowChangingEmail: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const persistedUser = await db.getUserByEmail(newEmail)
|
||||||
|
expect(persistedUser).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: user._id,
|
||||||
|
email: newEmail,
|
||||||
|
lastName: user.lastName,
|
||||||
|
_rev: expect.stringMatching(/^2-\w+/),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("updating emails frees previous emails", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const previousEmail = user.email
|
||||||
|
const newEmail = generator.email({})
|
||||||
|
expect(await searchExistingEmails([previousEmail, newEmail])).toEqual(
|
||||||
|
[previousEmail]
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.save(
|
||||||
|
{ ...user, email: newEmail },
|
||||||
|
{ allowChangingEmail: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(await searchExistingEmails([previousEmail, newEmail])).toEqual(
|
||||||
|
[newEmail]
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.save(
|
||||||
|
structures.users.user({
|
||||||
|
email: previousEmail,
|
||||||
|
tenantId: config.getTenantId(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(await searchExistingEmails([previousEmail, newEmail])).toEqual(
|
||||||
|
[previousEmail, newEmail]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,33 +1,25 @@
|
||||||
<script>
|
<script>
|
||||||
import Tooltip from "./Tooltip.svelte"
|
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
|
import AbsTooltip from "./AbsTooltip.svelte"
|
||||||
|
|
||||||
export let tooltip = ""
|
export let tooltip = ""
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let disabled = true
|
export let disabled = true
|
||||||
|
|
||||||
let showTooltip = false
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
<div class:container={!!tooltip}>
|
<div class:container={!!tooltip}>
|
||||||
<slot />
|
<slot />
|
||||||
{#if tooltip}
|
{#if tooltip}
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
|
<AbsTooltip text={tooltip}>
|
||||||
<div
|
<div
|
||||||
class="icon"
|
class="icon"
|
||||||
class:icon-small={size === "M" || size === "S"}
|
class:icon-small={size === "M" || size === "S"}
|
||||||
on:mouseover={() => (showTooltip = true)}
|
|
||||||
on:mouseleave={() => (showTooltip = false)}
|
|
||||||
on:focus
|
on:focus
|
||||||
>
|
>
|
||||||
<Icon name="InfoOutline" size="S" {disabled} />
|
<Icon name="InfoOutline" size="S" {disabled} hoverable />
|
||||||
</div>
|
</div>
|
||||||
{#if showTooltip}
|
</AbsTooltip>
|
||||||
<div class="tooltip">
|
|
||||||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,14 +36,6 @@
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
top: 15px;
|
|
||||||
z-index: 200;
|
|
||||||
width: 160px;
|
|
||||||
}
|
|
||||||
.icon {
|
.icon {
|
||||||
transform: scale(0.75);
|
transform: scale(0.75);
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,7 @@
|
||||||
This action cannot be undone.
|
This action cannot be undone.
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
<Modal bind:this={testDataModal} width="30%">
|
<Modal bind:this={testDataModal} width="30%" zIndex={5}>
|
||||||
<TestDataModal />
|
<TestDataModal />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
@ -148,7 +148,6 @@
|
||||||
.header.scrolling {
|
.header.scrolling {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
border-bottom: var(--border-light);
|
border-bottom: var(--border-light);
|
||||||
border-left: var(--border-light);
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,63 @@
|
||||||
import { automationStore, selectedAutomation } from "stores/builder"
|
import { automationStore, selectedAutomation } from "stores/builder"
|
||||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import { memo } from "@budibase/frontend-core"
|
||||||
|
import { AutomationEventType } from "@budibase/types"
|
||||||
|
|
||||||
let failedParse = null
|
let failedParse = null
|
||||||
let trigger = {}
|
let trigger = {}
|
||||||
let schemaProperties = {}
|
let schemaProperties = {}
|
||||||
|
|
||||||
|
const rowTriggers = [
|
||||||
|
AutomationEventType.ROW_DELETE,
|
||||||
|
AutomationEventType.ROW_UPDATE,
|
||||||
|
AutomationEventType.ROW_SAVE,
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the automation test data and ensures it is valid
|
||||||
|
* @param {object} testData contains all config for the test
|
||||||
|
* @returns {object} valid testData
|
||||||
|
* @todo Parse *all* data for each trigger type and relay adequate feedback
|
||||||
|
*/
|
||||||
|
const parseTestData = testData => {
|
||||||
|
const autoTrigger = $selectedAutomation?.definition?.trigger
|
||||||
|
const { tableId } = autoTrigger?.inputs || {}
|
||||||
|
|
||||||
|
// Ensure the tableId matches the trigger table for row trigger automations
|
||||||
|
if (
|
||||||
|
rowTriggers.includes(autoTrigger?.event) &&
|
||||||
|
testData?.row?.tableId !== tableId
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
// Reset Core fields
|
||||||
|
row: { tableId },
|
||||||
|
meta: {},
|
||||||
|
id: "",
|
||||||
|
revision: "",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Leave the core data as it is
|
||||||
|
return testData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Before executing a test run, relay if an automation is in a valid state
|
||||||
|
* @param {object} trigger The automation trigger config
|
||||||
|
* @returns {boolean} validation status
|
||||||
|
* @todo Parse *all* trigger types relay adequate feedback
|
||||||
|
*/
|
||||||
|
const isTriggerValid = trigger => {
|
||||||
|
if (rowTriggers.includes(trigger?.event) && !trigger?.inputs?.tableId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const memoTestData = memo(parseTestData($selectedAutomation.testData))
|
||||||
|
$: memoTestData.set(parseTestData($selectedAutomation.testData))
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
// clone the trigger so we're not mutating the reference
|
// clone the trigger so we're not mutating the reference
|
||||||
trigger = cloneDeep($selectedAutomation.definition.trigger)
|
trigger = cloneDeep($selectedAutomation.definition.trigger)
|
||||||
|
@ -20,34 +72,45 @@
|
||||||
// get the outputs so we can define the fields
|
// get the outputs so we can define the fields
|
||||||
let schema = Object.entries(trigger.schema?.outputs?.properties || {})
|
let schema = Object.entries(trigger.schema?.outputs?.properties || {})
|
||||||
|
|
||||||
if (trigger?.event === "app:trigger") {
|
if (trigger?.event === AutomationEventType.APP_TRIGGER) {
|
||||||
schema = [["fields", { customType: "fields" }]]
|
schema = [["fields", { customType: "fields" }]]
|
||||||
}
|
}
|
||||||
|
|
||||||
schemaProperties = schema
|
schemaProperties = schema
|
||||||
}
|
}
|
||||||
|
|
||||||
// check to see if there is existing test data in the store
|
|
||||||
$: testData = $selectedAutomation.testData || {}
|
|
||||||
|
|
||||||
// Check the schema to see if required fields have been entered
|
// Check the schema to see if required fields have been entered
|
||||||
$: isError = !trigger.schema.outputs.required.every(
|
$: isError =
|
||||||
required => testData[required] || required !== "row"
|
!isTriggerValid(trigger) ||
|
||||||
|
!trigger.schema.outputs.required.every(
|
||||||
|
required => $memoTestData?.[required] || required !== "row"
|
||||||
)
|
)
|
||||||
|
|
||||||
function parseTestJSON(e) {
|
function parseTestJSON(e) {
|
||||||
|
let jsonUpdate
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const obj = JSON.parse(e.detail)
|
jsonUpdate = JSON.parse(e.detail)
|
||||||
failedParse = null
|
failedParse = null
|
||||||
automationStore.actions.addTestDataToAutomation(obj)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
failedParse = "Invalid JSON"
|
failedParse = "Invalid JSON"
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rowTriggers.includes(trigger?.event)) {
|
||||||
|
const tableId = trigger?.inputs?.tableId
|
||||||
|
|
||||||
|
// Reset the tableId as it must match the trigger
|
||||||
|
if (jsonUpdate?.row?.tableId !== tableId) {
|
||||||
|
jsonUpdate.row.tableId = tableId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
automationStore.actions.addTestDataToAutomation(jsonUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
const testAutomation = async () => {
|
const testAutomation = async () => {
|
||||||
try {
|
try {
|
||||||
await automationStore.actions.test($selectedAutomation, testData)
|
await automationStore.actions.test($selectedAutomation, $memoTestData)
|
||||||
$automationStore.showTestPanel = true
|
$automationStore.showTestPanel = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(error)
|
notifications.error(error)
|
||||||
|
@ -85,7 +148,7 @@
|
||||||
{#if selectedValues}
|
{#if selectedValues}
|
||||||
<div class="tab-content-padding">
|
<div class="tab-content-padding">
|
||||||
<AutomationBlockSetup
|
<AutomationBlockSetup
|
||||||
{testData}
|
testData={$memoTestData}
|
||||||
{schemaProperties}
|
{schemaProperties}
|
||||||
isTestModal
|
isTestModal
|
||||||
block={trigger}
|
block={trigger}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import TableSelector from "./TableSelector.svelte"
|
import TableSelector from "./TableSelector.svelte"
|
||||||
import RowSelector from "./RowSelector.svelte"
|
|
||||||
import FieldSelector from "./FieldSelector.svelte"
|
import FieldSelector from "./FieldSelector.svelte"
|
||||||
import SchemaSetup from "./SchemaSetup.svelte"
|
import SchemaSetup from "./SchemaSetup.svelte"
|
||||||
|
import RowSelector from "./RowSelector.svelte"
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Input,
|
|
||||||
Select,
|
Select,
|
||||||
Label,
|
Label,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
|
@ -15,26 +14,27 @@
|
||||||
Checkbox,
|
Checkbox,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
|
Helpers,
|
||||||
Toggle,
|
Toggle,
|
||||||
Icon,
|
|
||||||
Divider,
|
Divider,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
||||||
import { environment, licensing } from "stores/portal"
|
import { environment, licensing } from "stores/portal"
|
||||||
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
||||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
import {
|
||||||
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
BindingSidePanel,
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
DrawerBindableSlot,
|
||||||
|
DrawerBindableInput,
|
||||||
|
ServerBindingPanel as AutomationBindingPanel,
|
||||||
|
ModalBindableInput,
|
||||||
|
} from "components/common/bindings"
|
||||||
import CodeEditorModal from "./CodeEditorModal.svelte"
|
import CodeEditorModal from "./CodeEditorModal.svelte"
|
||||||
import QuerySelector from "./QuerySelector.svelte"
|
|
||||||
import QueryParamSelector from "./QueryParamSelector.svelte"
|
import QueryParamSelector from "./QueryParamSelector.svelte"
|
||||||
import AutomationSelector from "./AutomationSelector.svelte"
|
import AutomationSelector from "./AutomationSelector.svelte"
|
||||||
import CronBuilder from "./CronBuilder.svelte"
|
import CronBuilder from "./CronBuilder.svelte"
|
||||||
import Editor from "components/integration/QueryEditor.svelte"
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
|
||||||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||||
import BindingSidePanel from "components/common/bindings/BindingSidePanel.svelte"
|
|
||||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
import { BindingHelpers, BindingType } from "components/common/bindings/utils"
|
import { BindingHelpers, BindingType } from "components/common/bindings/utils"
|
||||||
import {
|
import {
|
||||||
|
@ -43,31 +43,57 @@
|
||||||
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 } from "@budibase/frontend-core"
|
import { QueryUtils, Utils, search, memo } from "@budibase/frontend-core"
|
||||||
import {
|
import {
|
||||||
getSchemaForDatasourcePlus,
|
getSchemaForDatasourcePlus,
|
||||||
getEnvironmentBindings,
|
getEnvironmentBindings,
|
||||||
} from "dataBinding"
|
} from "dataBinding"
|
||||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import {
|
||||||
|
AutomationEventType,
|
||||||
|
AutomationStepType,
|
||||||
|
AutomationActionStepId,
|
||||||
|
} from "@budibase/types"
|
||||||
import { FIELDS } from "constants/backend"
|
import { FIELDS } from "constants/backend"
|
||||||
|
import PropField from "./PropField.svelte"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let testData
|
export let testData
|
||||||
export let schemaProperties
|
export let schemaProperties
|
||||||
export let isTestModal = false
|
export let isTestModal = false
|
||||||
|
|
||||||
|
// Stop unnecessary rendering
|
||||||
|
const memoBlock = memo(block)
|
||||||
|
|
||||||
|
const rowTriggers = [
|
||||||
|
TriggerStepID.ROW_UPDATED,
|
||||||
|
TriggerStepID.ROW_SAVED,
|
||||||
|
TriggerStepID.ROW_DELETED,
|
||||||
|
]
|
||||||
|
|
||||||
|
const rowEvents = [
|
||||||
|
AutomationEventType.ROW_DELETE,
|
||||||
|
AutomationEventType.ROW_SAVE,
|
||||||
|
AutomationEventType.ROW_UPDATE,
|
||||||
|
]
|
||||||
|
|
||||||
|
const rowSteps = [ActionStepID.UPDATE_ROW, ActionStepID.CREATE_ROW]
|
||||||
|
|
||||||
let webhookModal
|
let webhookModal
|
||||||
let drawer
|
let drawer
|
||||||
let inputData
|
let inputData
|
||||||
let insertAtPos, getCaretPosition
|
let insertAtPos, getCaretPosition
|
||||||
|
let stepLayouts = {}
|
||||||
|
|
||||||
|
$: memoBlock.set(block)
|
||||||
$: filters = lookForFilters(schemaProperties) || []
|
$: filters = lookForFilters(schemaProperties) || []
|
||||||
$: tempFilters = filters
|
$: tempFilters = filters
|
||||||
$: stepId = block.stepId
|
$: stepId = block.stepId
|
||||||
$: bindings = getAvailableBindings(block, $selectedAutomation?.definition)
|
$: bindings = getAvailableBindings(block, $selectedAutomation?.definition)
|
||||||
$: getInputData(testData, block.inputs)
|
$: getInputData(testData, $memoBlock.inputs)
|
||||||
$: tableId = inputData ? inputData.tableId : null
|
$: tableId = inputData ? inputData.tableId : null
|
||||||
$: table = tableId
|
$: table = tableId
|
||||||
? $tables.list.find(table => table._id === inputData.tableId)
|
? $tables.list.find(table => table._id === inputData.tableId)
|
||||||
|
@ -81,31 +107,33 @@
|
||||||
{ allowLinks: true }
|
{ allowLinks: true }
|
||||||
)
|
)
|
||||||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||||
$: isTrigger = block?.type === "TRIGGER"
|
$: isTrigger = block?.type === AutomationStepType.TRIGGER
|
||||||
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
|
||||||
$: codeMode =
|
$: codeMode =
|
||||||
stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS
|
stepId === AutomationActionStepId.EXECUTE_BASH
|
||||||
|
? EditorModes.Handlebars
|
||||||
|
: EditorModes.JS
|
||||||
$: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, {
|
$: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, {
|
||||||
disableWrapping: true,
|
disableWrapping: true,
|
||||||
})
|
})
|
||||||
$: editingJs = codeMode === EditorModes.JS
|
$: editingJs = codeMode === EditorModes.JS
|
||||||
$: requiredProperties = block.schema.inputs.required || []
|
$: requiredProperties = isTestModal ? [] : block.schema["inputs"].required
|
||||||
|
|
||||||
$: stepCompletions =
|
$: stepCompletions =
|
||||||
codeMode === EditorModes.Handlebars
|
codeMode === EditorModes.Handlebars
|
||||||
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||||
: []
|
: []
|
||||||
|
|
||||||
let testDataRowVisibility = {}
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
// Ensures the app action fields are populated
|
// Ensures the app action fields are populated
|
||||||
if (block.event === "app:trigger" && !newInputData?.fields) {
|
if (
|
||||||
|
block.event === AutomationEventType.APP_TRIGGER &&
|
||||||
|
!newInputData?.fields
|
||||||
|
) {
|
||||||
newInputData = cloneDeep(blockInputs)
|
newInputData = cloneDeep(blockInputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
inputData = newInputData
|
inputData = newInputData
|
||||||
setDefaultEnumValues()
|
setDefaultEnumValues()
|
||||||
}
|
}
|
||||||
|
@ -117,15 +145,338 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const onChange = Utils.sequential(async (e, key) => {
|
|
||||||
|
// Store for any UX related data
|
||||||
|
const stepStore = writable({})
|
||||||
|
$: currentStep = $stepStore?.[block.id]
|
||||||
|
|
||||||
|
$: customStepLayouts($memoBlock, schemaProperties, currentStep)
|
||||||
|
|
||||||
|
const customStepLayouts = block => {
|
||||||
|
if (
|
||||||
|
rowSteps.includes(block.stepId) ||
|
||||||
|
(rowTriggers.includes(block.stepId) && isTestModal)
|
||||||
|
) {
|
||||||
|
const schema = schemaProperties.reduce((acc, entry) => {
|
||||||
|
const [key, val] = entry
|
||||||
|
acc[key] = val
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
// Optionally build the rev field config when its needed.
|
||||||
|
const getRevConfig = () => {
|
||||||
|
const rowRevEntry = schema["revision"]
|
||||||
|
if (!rowRevEntry) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const rowRevlabel = getFieldLabel("revision", rowRevEntry)
|
||||||
|
|
||||||
|
return isTestModal
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: DrawerBindableInput,
|
||||||
|
title: rowRevlabel,
|
||||||
|
props: {
|
||||||
|
panel: AutomationBindingPanel,
|
||||||
|
value: inputData["revision"],
|
||||||
|
onChange: e => {
|
||||||
|
onChange({ ["revision"]: e.detail })
|
||||||
|
},
|
||||||
|
bindings,
|
||||||
|
updateOnChange: false,
|
||||||
|
forceModal: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIdConfig = () => {
|
||||||
|
const rowIdentifier = isTestModal ? "id" : "rowId"
|
||||||
|
|
||||||
|
const rowIdEntry = schema[rowIdentifier]
|
||||||
|
if (!rowIdEntry) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowIdlabel = getFieldLabel(rowIdentifier, rowIdEntry)
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: DrawerBindableInput,
|
||||||
|
title: rowIdlabel,
|
||||||
|
props: {
|
||||||
|
panel: AutomationBindingPanel,
|
||||||
|
value: inputData[rowIdentifier],
|
||||||
|
onChange: e => {
|
||||||
|
onChange({ [rowIdentifier]: e.detail })
|
||||||
|
},
|
||||||
|
bindings,
|
||||||
|
updateOnChange: false,
|
||||||
|
forceModal: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// A select to switch from `row` to `oldRow`
|
||||||
|
const getRowTypeConfig = () => {
|
||||||
|
if (!isTestModal || block.event !== AutomationEventType.ROW_UPDATE) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$stepStore?.[block.id]) {
|
||||||
|
stepStore.update(state => ({
|
||||||
|
...state,
|
||||||
|
[block.id]: {
|
||||||
|
rowType: "row",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: Select,
|
||||||
|
tooltip: `You can configure test data for both the updated row and
|
||||||
|
the old row, if you need it. Just select the one you wish to alter`,
|
||||||
|
title: "Row data",
|
||||||
|
props: {
|
||||||
|
value: $stepStore?.[block.id].rowType,
|
||||||
|
onChange: e => {
|
||||||
|
stepStore.update(state => ({
|
||||||
|
...state,
|
||||||
|
[block.id]: {
|
||||||
|
rowType: e.detail,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
getOptionLabel: type => type.name,
|
||||||
|
getOptionValue: type => type.id,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: "row",
|
||||||
|
name: "Updated row",
|
||||||
|
},
|
||||||
|
{ id: "oldRow", name: "Old row" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRowSelector = () => {
|
||||||
|
const baseProps = {
|
||||||
|
bindings,
|
||||||
|
isTestModal,
|
||||||
|
isUpdateRow: block.stepId === ActionStepID.UPDATE_ROW,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTestModal && currentStep?.rowType === "oldRow") {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: RowSelector,
|
||||||
|
props: {
|
||||||
|
row: inputData["oldRow"] || {
|
||||||
|
tableId: inputData["row"].tableId,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
fields: inputData["meta"].oldFields || {},
|
||||||
|
},
|
||||||
|
onChange: e => {
|
||||||
|
onChange({
|
||||||
|
oldRow: e.detail.row,
|
||||||
|
meta: {
|
||||||
|
fields: inputData["meta"].fields,
|
||||||
|
oldFields: e.detail.meta.fields,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
...baseProps,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: RowSelector,
|
||||||
|
props: {
|
||||||
|
row: inputData["row"],
|
||||||
|
meta: inputData["meta"] || {},
|
||||||
|
onChange: e => {
|
||||||
|
onChange(e.detail)
|
||||||
|
},
|
||||||
|
...baseProps,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
stepLayouts[block.stepId] = {
|
||||||
|
row: {
|
||||||
|
schema: schema["row"],
|
||||||
|
//?layout: RowLayoutStepComponent.
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: TableSelector,
|
||||||
|
title: "Table",
|
||||||
|
props: {
|
||||||
|
isTrigger,
|
||||||
|
value: inputData["row"]?.tableId ?? "",
|
||||||
|
onChange: e => {
|
||||||
|
const rowKey = $stepStore?.[block.id]?.rowType || "row"
|
||||||
|
onChange({
|
||||||
|
_tableId: e.detail,
|
||||||
|
meta: {},
|
||||||
|
[rowKey]: e.detail
|
||||||
|
? {
|
||||||
|
tableId: e.detail,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
disabled: isTestModal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...getIdConfig(),
|
||||||
|
...getRevConfig(),
|
||||||
|
...getRowTypeConfig(),
|
||||||
|
{
|
||||||
|
type: Divider,
|
||||||
|
props: {
|
||||||
|
noMargin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...getRowSelector(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for row trigger automation updates.
|
||||||
|
@param {object} update - An automation block.inputs update object
|
||||||
|
@example
|
||||||
|
onRowTriggerUpdate({
|
||||||
|
"tableId" : "ta_bb_employee"
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
const onRowTriggerUpdate = async update => {
|
||||||
|
if (
|
||||||
|
Object.hasOwn(update, "tableId") &&
|
||||||
|
$selectedAutomation.testData?.row?.tableId !== update.tableId
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const reqSchema = getSchemaForDatasourcePlus(update.tableId, {
|
||||||
|
searchableSchema: true,
|
||||||
|
}).schema
|
||||||
|
|
||||||
|
// Parse the block inputs as usual
|
||||||
|
const updatedAutomation =
|
||||||
|
await automationStore.actions.processBlockInputs(block, {
|
||||||
|
schema: reqSchema,
|
||||||
|
...update,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Save the entire automation and reset the testData
|
||||||
|
await automationStore.actions.save({
|
||||||
|
...updatedAutomation,
|
||||||
|
testData: {
|
||||||
|
// Reset Core fields
|
||||||
|
row: { tableId: update.tableId },
|
||||||
|
oldRow: { tableId: update.tableId },
|
||||||
|
meta: {},
|
||||||
|
id: "",
|
||||||
|
revision: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error saving automation", e)
|
||||||
|
notifications.error("Error saving automation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for App trigger automation updates.
|
||||||
|
* Ensure updates to the field list are reflected in testData
|
||||||
|
@param {object} update - An app trigger update object
|
||||||
|
@example
|
||||||
|
onAppTriggerUpdate({
|
||||||
|
"fields" : {"myField": "123", "myArray": "cat,dog,badger"}
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
const onAppTriggerUpdate = async update => {
|
||||||
|
try {
|
||||||
|
// Parse the block inputs as usual
|
||||||
|
const updatedAutomation =
|
||||||
|
await automationStore.actions.processBlockInputs(block, {
|
||||||
|
schema: {},
|
||||||
|
...update,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Exclude default or invalid data from the test data
|
||||||
|
let updatedFields = {}
|
||||||
|
for (const key of Object.keys(block?.inputs?.fields || {})) {
|
||||||
|
if (Object.hasOwn(update.fields, key)) {
|
||||||
|
if (key !== "") {
|
||||||
|
updatedFields[key] = updatedAutomation.testData?.fields?.[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the entire automation and reset the testData
|
||||||
|
await automationStore.actions.save({
|
||||||
|
...updatedAutomation,
|
||||||
|
testData: {
|
||||||
|
fields: updatedFields,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error saving automation", e)
|
||||||
|
notifications.error("Error saving automation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for automation block input updates.
|
||||||
|
@param {object} update - An automation inputs update object
|
||||||
|
@example
|
||||||
|
onChange({
|
||||||
|
meta: { fields : { "Photo": { useAttachmentBinding: false }} }
|
||||||
|
row: { "Active": true, "Order Id" : 14, ... }
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
const onChange = Utils.sequential(async update => {
|
||||||
|
const request = cloneDeep(update)
|
||||||
|
|
||||||
|
// Process app trigger updates
|
||||||
|
if (isTrigger && !isTestModal) {
|
||||||
|
// Row trigger
|
||||||
|
if (rowEvents.includes(block.event)) {
|
||||||
|
await onRowTriggerUpdate(request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// App trigger
|
||||||
|
if (block.event === AutomationEventType.APP_TRIGGER) {
|
||||||
|
await onAppTriggerUpdate(request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We need to cache the schema as part of the definition because it is
|
// We need to cache the schema as part of the definition because it is
|
||||||
// used in the server to detect relationships. It would be far better to
|
// used in the server to detect relationships. It would be far better to
|
||||||
// instead fetch the schema in the backend at runtime.
|
// instead fetch the schema in the backend at runtime.
|
||||||
|
// If _tableId is explicitly included in the update request, the schema will be requested
|
||||||
let schema
|
let schema
|
||||||
if (e.detail?.tableId) {
|
if (request?._tableId) {
|
||||||
schema = getSchemaForDatasourcePlus(e.detail.tableId, {
|
schema = getSchemaForDatasourcePlus(request._tableId, {
|
||||||
searchableSchema: true,
|
searchableSchema: true,
|
||||||
}).schema
|
}).schema
|
||||||
|
delete request._tableId
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (isTestModal) {
|
if (isTestModal) {
|
||||||
|
@ -136,21 +487,22 @@
|
||||||
newTestData = {
|
newTestData = {
|
||||||
...newTestData,
|
...newTestData,
|
||||||
body: {
|
body: {
|
||||||
[key]: e.detail,
|
...update,
|
||||||
...$selectedAutomation.testData?.body,
|
...$selectedAutomation.testData?.body,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newTestData = {
|
newTestData = {
|
||||||
...newTestData,
|
...newTestData,
|
||||||
[key]: e.detail,
|
...request,
|
||||||
}
|
}
|
||||||
await automationStore.actions.addTestDataToAutomation(newTestData)
|
await automationStore.actions.addTestDataToAutomation(newTestData)
|
||||||
} else {
|
} else {
|
||||||
const data = { schema, [key]: e.detail }
|
const data = { schema, ...request }
|
||||||
await automationStore.actions.updateBlockInputs(block, data)
|
await automationStore.actions.updateBlockInputs(block, data)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Error saving automation", error)
|
||||||
notifications.error("Error saving automation")
|
notifications.error("Error saving automation")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -195,14 +547,17 @@
|
||||||
let runtimeName
|
let runtimeName
|
||||||
|
|
||||||
/* Begin special cases for generating custom schemas based on triggers */
|
/* Begin special cases for generating custom schemas based on triggers */
|
||||||
if (idx === 0 && automation.trigger?.event === "app:trigger") {
|
if (
|
||||||
|
idx === 0 &&
|
||||||
|
automation.trigger?.event === AutomationEventType.APP_TRIGGER
|
||||||
|
) {
|
||||||
return `trigger.fields.${name}`
|
return `trigger.fields.${name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
idx === 0 &&
|
idx === 0 &&
|
||||||
(automation.trigger?.event === "row:update" ||
|
(automation.trigger?.event === AutomationEventType.ROW_UPDATE ||
|
||||||
automation.trigger?.event === "row:save")
|
automation.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 `trigger.row.${name}`
|
||||||
|
@ -277,7 +632,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (idx === 0 && automation.trigger?.event === "app:trigger") {
|
if (
|
||||||
|
idx === 0 &&
|
||||||
|
automation.trigger?.event === AutomationEventType.APP_TRIGGER
|
||||||
|
) {
|
||||||
schema = Object.fromEntries(
|
schema = Object.fromEntries(
|
||||||
Object.keys(automation.trigger.inputs.fields || []).map(key => [
|
Object.keys(automation.trigger.inputs.fields || []).map(key => [
|
||||||
key,
|
key,
|
||||||
|
@ -286,8 +644,9 @@
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
(idx === 0 && automation.trigger.event === "row:update") ||
|
(idx === 0 &&
|
||||||
(idx === 0 && automation.trigger.event === "row:save")
|
automation.trigger.event === AutomationEventType.ROW_UPDATE) ||
|
||||||
|
(idx === 0 && automation.trigger.event === AutomationEventType.ROW_SAVE)
|
||||||
) {
|
) {
|
||||||
let table = $tables.list.find(
|
let table = $tables.list.find(
|
||||||
table => table._id === automation.trigger.inputs.tableId
|
table => table._id === automation.trigger.inputs.tableId
|
||||||
|
@ -353,10 +712,12 @@
|
||||||
|
|
||||||
function saveFilters(key) {
|
function saveFilters(key) {
|
||||||
const filters = QueryUtils.buildQuery(tempFilters)
|
const filters = QueryUtils.buildQuery(tempFilters)
|
||||||
const defKey = `${key}-def`
|
|
||||||
onChange({ detail: filters }, key)
|
onChange({
|
||||||
// need to store the builder definition in the automation
|
[key]: filters,
|
||||||
onChange({ detail: tempFilters }, defKey)
|
[`${key}-def`]: tempFilters, // need to store the builder definition in the automation
|
||||||
|
})
|
||||||
|
|
||||||
drawer.hide()
|
drawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,6 +734,7 @@
|
||||||
value.customType !== "cron" &&
|
value.customType !== "cron" &&
|
||||||
value.customType !== "triggerSchema" &&
|
value.customType !== "triggerSchema" &&
|
||||||
value.customType !== "automationFields" &&
|
value.customType !== "automationFields" &&
|
||||||
|
value.customType !== "fields" &&
|
||||||
value.type !== "signature_single" &&
|
value.type !== "signature_single" &&
|
||||||
value.type !== "attachment" &&
|
value.type !== "attachment" &&
|
||||||
value.type !== "attachment_single"
|
value.type !== "attachment_single"
|
||||||
|
@ -381,11 +743,10 @@
|
||||||
|
|
||||||
function getFieldLabel(key, value) {
|
function getFieldLabel(key, value) {
|
||||||
const requiredSuffix = requiredProperties.includes(key) ? "*" : ""
|
const requiredSuffix = requiredProperties.includes(key) ? "*" : ""
|
||||||
return `${value.title || (key === "row" ? "Row" : key)} ${requiredSuffix}`
|
const label = `${
|
||||||
}
|
value.title || (key === "row" ? "Row" : key)
|
||||||
|
} ${requiredSuffix}`
|
||||||
function toggleTestDataRowVisibility(key) {
|
return Helpers.capitalise(label)
|
||||||
testDataRowVisibility[key] = !testDataRowVisibility[key]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAttachmentParams(keyValueObj) {
|
function handleAttachmentParams(keyValueObj) {
|
||||||
|
@ -398,16 +759,6 @@
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAttachmentBinding(e, key) {
|
|
||||||
onChange(
|
|
||||||
{
|
|
||||||
detail: "",
|
|
||||||
},
|
|
||||||
key
|
|
||||||
)
|
|
||||||
onChange({ detail: { useAttachmentBinding: e.detail } }, "meta")
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await environment.loadVariables()
|
await environment.loadVariables()
|
||||||
|
@ -417,7 +768,32 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fields">
|
<div class="step-fields">
|
||||||
|
<!-- Custom Layouts -->
|
||||||
|
{#if stepLayouts[block.stepId]}
|
||||||
|
{#each Object.keys(stepLayouts[block.stepId] || {}) as key}
|
||||||
|
{#if canShowField(key, stepLayouts[block.stepId].schema)}
|
||||||
|
{#each stepLayouts[block.stepId][key].content as config}
|
||||||
|
{#if config.title}
|
||||||
|
<PropField label={config.title} labelTooltip={config.tooltip}>
|
||||||
|
<svelte:component
|
||||||
|
this={config.type}
|
||||||
|
{...config.props}
|
||||||
|
on:change={config.props.onChange}
|
||||||
|
/>
|
||||||
|
</PropField>
|
||||||
|
{:else}
|
||||||
|
<svelte:component
|
||||||
|
this={config.type}
|
||||||
|
{...config.props}
|
||||||
|
on:change={config.props.onChange}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<!-- Default Schema Property Layout -->
|
||||||
{#each schemaProperties as [key, value]}
|
{#each schemaProperties as [key, value]}
|
||||||
{#if canShowField(key, value)}
|
{#if canShowField(key, value)}
|
||||||
{@const label = getFieldLabel(key, value)}
|
{@const label = getFieldLabel(key, value)}
|
||||||
|
@ -426,13 +802,15 @@
|
||||||
<Label
|
<Label
|
||||||
tooltip={value.title === "Binding / Value"
|
tooltip={value.title === "Binding / Value"
|
||||||
? "If using the String input type, please use a comma or newline separated string"
|
? "If using the String input type, please use a comma or newline separated string"
|
||||||
: null}>{label}</Label
|
: null}
|
||||||
>
|
>
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
{/if}
|
{/if}
|
||||||
<div class:field-width={shouldRenderField(value)}>
|
<div class:field-width={shouldRenderField(value)}>
|
||||||
{#if value.type === "string" && value.enum && canShowField(key, value)}
|
{#if value.type === "string" && value.enum && canShowField(key, value)}
|
||||||
<Select
|
<Select
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
placeholder={false}
|
placeholder={false}
|
||||||
options={value.enum}
|
options={value.enum}
|
||||||
|
@ -445,16 +823,14 @@
|
||||||
editorWidth="448"
|
editorWidth="448"
|
||||||
mode="json"
|
mode="json"
|
||||||
value={inputData[key]?.value}
|
value={inputData[key]?.value}
|
||||||
on:change={e => {
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
onChange(e, key)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{:else if value.type === "boolean"}
|
{:else if value.type === "boolean"}
|
||||||
<div style="margin-top: 10px">
|
<div style="margin-top: 10px">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
text={value.title}
|
text={value.title}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if value.type === "date"}
|
{:else if value.type === "date"}
|
||||||
|
@ -463,7 +839,7 @@
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type={"date"}
|
type={"date"}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
{bindings}
|
{bindings}
|
||||||
allowJS={true}
|
allowJS={true}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
|
@ -471,12 +847,12 @@
|
||||||
>
|
>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
/>
|
/>
|
||||||
</DrawerBindableSlot>
|
</DrawerBindableSlot>
|
||||||
{:else if value.customType === "column"}
|
{:else if value.customType === "column"}
|
||||||
<Select
|
<Select
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
options={Object.keys(table?.schema || {})}
|
options={Object.keys(table?.schema || {})}
|
||||||
/>
|
/>
|
||||||
|
@ -490,7 +866,14 @@
|
||||||
value={inputData?.meta?.useAttachmentBinding}
|
value={inputData?.meta?.useAttachmentBinding}
|
||||||
text={"Use bindings"}
|
text={"Use bindings"}
|
||||||
size={"XS"}
|
size={"XS"}
|
||||||
on:change={e => toggleAttachmentBinding(e, key)}
|
on:change={e => {
|
||||||
|
onChange({
|
||||||
|
[key]: null,
|
||||||
|
meta: {
|
||||||
|
useAttachmentBinding: e.detail,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -498,20 +881,19 @@
|
||||||
{#if !inputData?.meta?.useAttachmentBinding}
|
{#if !inputData?.meta?.useAttachmentBinding}
|
||||||
<KeyValueBuilder
|
<KeyValueBuilder
|
||||||
on:change={e =>
|
on:change={e =>
|
||||||
onChange(
|
onChange({
|
||||||
{
|
[key]: e.detail.map(({ name, value }) => ({
|
||||||
detail: e.detail.map(({ name, value }) => ({
|
|
||||||
url: name,
|
url: name,
|
||||||
filename: value,
|
filename: value,
|
||||||
})),
|
})),
|
||||||
},
|
})}
|
||||||
key
|
|
||||||
)}
|
|
||||||
object={handleAttachmentParams(inputData[key])}
|
object={handleAttachmentParams(inputData[key])}
|
||||||
allowJS
|
allowJS
|
||||||
{bindings}
|
{bindings}
|
||||||
keyBindings
|
keyBindings
|
||||||
customButtonText={"Add attachment"}
|
customButtonText={value.type === "attachment"
|
||||||
|
? "Add attachment"
|
||||||
|
: "Add signature"}
|
||||||
keyPlaceholder={"URL"}
|
keyPlaceholder={"URL"}
|
||||||
valuePlaceholder={"Filename"}
|
valuePlaceholder={"Filename"}
|
||||||
/>
|
/>
|
||||||
|
@ -521,18 +903,17 @@
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type={value.customType}
|
type={value.customType}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
{bindings}
|
{bindings}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="test">
|
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
title={value.title ?? label}
|
title={value.title ?? label}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type={value.customType}
|
type={value.customType}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
{bindings}
|
{bindings}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
placeholder={value.customType === "queryLimit"
|
placeholder={value.customType === "queryLimit"
|
||||||
|
@ -540,7 +921,6 @@
|
||||||
: ""}
|
: ""}
|
||||||
drawerLeft="260px"
|
drawerLeft="260px"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -561,55 +941,20 @@
|
||||||
/>
|
/>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
{:else if value.customType === "password"}
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
value={inputData[key]}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "email"}
|
|
||||||
{#if isTestModal}
|
|
||||||
<ModalBindableInput
|
|
||||||
title={value.title ?? label}
|
|
||||||
value={inputData[key]}
|
|
||||||
panel={AutomationBindingPanel}
|
|
||||||
type="email"
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
{bindings}
|
|
||||||
updateOnChange={false}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<DrawerBindableInput
|
|
||||||
title={value.title ?? label}
|
|
||||||
panel={AutomationBindingPanel}
|
|
||||||
type="email"
|
|
||||||
value={inputData[key]}
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
{bindings}
|
|
||||||
allowJS={false}
|
|
||||||
updateOnChange={false}
|
|
||||||
drawerLeft="260px"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{:else if value.customType === "query"}
|
|
||||||
<QuerySelector
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
value={inputData[key]}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "cron"}
|
{:else if value.customType === "cron"}
|
||||||
<CronBuilder
|
<CronBuilder
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "automationFields"}
|
{:else if value.customType === "automationFields"}
|
||||||
<AutomationSelector
|
<AutomationSelector
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
{bindings}
|
{bindings}
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "queryParams"}
|
{:else if value.customType === "queryParams"}
|
||||||
<QueryParamSelector
|
<QueryParamSelector
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
{bindings}
|
{bindings}
|
||||||
/>
|
/>
|
||||||
|
@ -617,78 +962,35 @@
|
||||||
<TableSelector
|
<TableSelector
|
||||||
{isTrigger}
|
{isTrigger}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "row"}
|
|
||||||
{#if isTestModal}
|
|
||||||
<div class="align-horizontally">
|
|
||||||
<Icon
|
|
||||||
name={testDataRowVisibility[key] ? "Remove" : "Add"}
|
|
||||||
hoverable
|
|
||||||
on:click={() => toggleTestDataRowVisibility(key)}
|
|
||||||
/>
|
|
||||||
<Label size="XL">{label}</Label>
|
|
||||||
</div>
|
|
||||||
{#if testDataRowVisibility[key]}
|
|
||||||
<RowSelector
|
|
||||||
value={inputData[key]}
|
|
||||||
meta={inputData["meta"] || {}}
|
|
||||||
on:change={e => {
|
|
||||||
if (e.detail?.key) {
|
|
||||||
onChange(e, e.detail.key)
|
|
||||||
} else {
|
|
||||||
onChange(e, key)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{bindings}
|
|
||||||
{isTestModal}
|
|
||||||
{isUpdateRow}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<Divider />
|
|
||||||
{:else}
|
|
||||||
<RowSelector
|
|
||||||
value={inputData[key]}
|
|
||||||
meta={inputData["meta"] || {}}
|
|
||||||
on:change={e => {
|
|
||||||
if (e.detail?.key) {
|
|
||||||
onChange(e, e.detail.key)
|
|
||||||
} else {
|
|
||||||
onChange(e, key)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{bindings}
|
|
||||||
{isTestModal}
|
|
||||||
{isUpdateRow}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{:else if value.customType === "webhookUrl"}
|
{:else if value.customType === "webhookUrl"}
|
||||||
<WebhookDisplay
|
<WebhookDisplay value={inputData[key]} />
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
value={inputData[key]}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "fields"}
|
{:else if value.customType === "fields"}
|
||||||
<FieldSelector
|
<FieldSelector
|
||||||
{block}
|
{block}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
{bindings}
|
{bindings}
|
||||||
{isTestModal}
|
{isTestModal}
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "triggerSchema"}
|
{:else if value.customType === "triggerSchema"}
|
||||||
<SchemaSetup
|
<SchemaSetup
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "code"}
|
{:else if value.customType === "code"}
|
||||||
<CodeEditorModal>
|
<CodeEditorModal>
|
||||||
<div class:js-editor={editingJs}>
|
<div class:js-editor={editingJs}>
|
||||||
<div class:js-code={editingJs} style="width:100%;height:500px;">
|
<div
|
||||||
|
class:js-code={editingJs}
|
||||||
|
style="width:100%;height:500px;"
|
||||||
|
>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
// need to pass without the value inside
|
// need to pass without the value inside
|
||||||
onChange({ detail: e.detail }, key)
|
onChange({ [key]: e.detail })
|
||||||
inputData[key] = e.detail
|
inputData[key] = e.detail
|
||||||
}}
|
}}
|
||||||
completions={stepCompletions}
|
completions={stepCompletions}
|
||||||
|
@ -724,7 +1026,7 @@
|
||||||
</CodeEditorModal>
|
</CodeEditorModal>
|
||||||
{:else if value.customType === "loopOption"}
|
{:else if value.customType === "loopOption"}
|
||||||
<Select
|
<Select
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
autoWidth
|
autoWidth
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
options={["Array", "String"]}
|
options={["Array", "String"]}
|
||||||
|
@ -737,18 +1039,18 @@
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type={value.customType}
|
type={value.customType}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
{bindings}
|
{bindings}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="test">
|
<div>
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
title={value.title ?? label}
|
title={value.title ?? label}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type={value.customType}
|
type={value.customType}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
{bindings}
|
{bindings}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
placeholder={value.customType === "queryLimit"
|
placeholder={value.customType === "queryLimit"
|
||||||
|
@ -763,12 +1065,14 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal bind:this={webhookModal} width="30%">
|
<Modal bind:this={webhookModal} width="30%">
|
||||||
<CreateWebhookModal />
|
<CreateWebhookModal />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{#if stepId === TriggerStepID.WEBHOOK}
|
{#if stepId === TriggerStepID.WEBHOOK && !isTestModal}
|
||||||
<Button secondary on:click={() => webhookModal.show()}>Set Up Webhook</Button>
|
<Button secondary on:click={() => webhookModal.show()}>Set Up Webhook</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -777,18 +1081,12 @@
|
||||||
width: 320px;
|
width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.align-horizontally {
|
.step-fields {
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fields {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-field {
|
.block-field {
|
||||||
|
@ -808,10 +1106,6 @@
|
||||||
margin-top: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.test :global(.drawer) {
|
|
||||||
width: 10000px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-editor {
|
.js-editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -1,19 +1,28 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
import PropField from "./PropField.svelte"
|
||||||
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
|
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
|
||||||
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
|
import { DatePicker, Select } from "@budibase/bbui"
|
||||||
|
import { FieldType } from "@budibase/types"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let value
|
export let value = {}
|
||||||
export let bindings
|
export let bindings
|
||||||
export let block
|
export let block
|
||||||
export let isTestModal
|
export let isTestModal
|
||||||
|
|
||||||
let schemaFields
|
const { STRING, NUMBER, ARRAY } = FieldType
|
||||||
|
|
||||||
|
let schemaFields = []
|
||||||
|
let editableValue
|
||||||
|
|
||||||
|
$: editableValue = { ...value }
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
let fields = {}
|
let fields = {}
|
||||||
|
|
||||||
for (const [key, type] of Object.entries(block?.inputs?.fields ?? {})) {
|
for (const [key, type] of Object.entries(block?.inputs?.fields ?? {})) {
|
||||||
fields = {
|
fields = {
|
||||||
...fields,
|
...fields,
|
||||||
|
@ -25,8 +34,8 @@
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value[key] === type) {
|
if (editableValue[key] === type) {
|
||||||
value[key] = INITIAL_VALUES[type.toUpperCase()]
|
editableValue[key] = INITIAL_VALUES[type.toUpperCase()]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,77 +47,58 @@
|
||||||
NUMBER: null,
|
NUMBER: null,
|
||||||
DATETIME: null,
|
DATETIME: null,
|
||||||
STRING: "",
|
STRING: "",
|
||||||
OPTIONS: [],
|
ARRAY: "",
|
||||||
ARRAY: [],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const coerce = (value, type) => {
|
const onChange = (e, field) => {
|
||||||
const re = new RegExp(/{{([^{].*?)}}/g)
|
if (e.detail !== editableValue[field]) {
|
||||||
if (re.test(value)) {
|
editableValue[field] = e.detail
|
||||||
return value
|
dispatch("change", editableValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "boolean") {
|
|
||||||
if (typeof value === "boolean") {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return value === "true"
|
|
||||||
}
|
|
||||||
if (type === "number") {
|
|
||||||
if (typeof value === "number") {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return Number(value)
|
|
||||||
}
|
|
||||||
if (type === "options") {
|
|
||||||
return [value]
|
|
||||||
}
|
|
||||||
if (type === "array") {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return value.split(",").map(x => x.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "link") {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
return [value]
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
const onChange = (e, field, type) => {
|
|
||||||
value[field] = coerce(e.detail, type)
|
|
||||||
dispatch("change", value)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if schemaFields.length && isTestModal}
|
{#if schemaFields?.length && isTestModal}
|
||||||
<div class="schema-fields">
|
<div class="fields">
|
||||||
{#each schemaFields as [field, schema]}
|
{#each schemaFields as [field, schema]}
|
||||||
<RowSelectorTypes
|
<PropField label={field}>
|
||||||
{isTestModal}
|
{#if [STRING, NUMBER, ARRAY].includes(schema.type)}
|
||||||
{field}
|
<svelte:component
|
||||||
{schema}
|
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
value={editableValue[field]}
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
|
type="string"
|
||||||
{bindings}
|
{bindings}
|
||||||
{value}
|
allowJS={true}
|
||||||
{onChange}
|
updateOnChange={false}
|
||||||
|
title={schema.name}
|
||||||
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
{:else if schema.type === "boolean"}
|
||||||
|
<Select
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
|
value={editableValue[field]}
|
||||||
|
options={[
|
||||||
|
{ label: "True", value: "true" },
|
||||||
|
{ label: "False", value: "false" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{:else if schema.type === "datetime"}
|
||||||
|
<DatePicker
|
||||||
|
value={editableValue[field]}
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</PropField>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.schema-fields {
|
.fields {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-gap: var(--spacing-s);
|
flex-direction: column;
|
||||||
margin-top: var(--spacing-s);
|
gap: var(--spacing-m);
|
||||||
}
|
|
||||||
.schema-fields :global(label) {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
<script>
|
||||||
|
import { Label } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let label
|
||||||
|
export let labelTooltip
|
||||||
|
export let fullWidth = false
|
||||||
|
export let componentWidth = 320
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="prop-field"
|
||||||
|
class:fullWidth
|
||||||
|
style={`--comp-width: ${componentWidth}px;`}
|
||||||
|
>
|
||||||
|
<div class="prop-label" title={label}>
|
||||||
|
<Label tooltip={labelTooltip}>{label}</Label>
|
||||||
|
</div>
|
||||||
|
<div class="prop-control">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.prop-field {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr var(--comp-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-field.fullWidth {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-field.fullWidth .prop-label {
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-label :global(> div) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-label :global(> div > label) {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-control {
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-field.fullWidth .prop-control {
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,28 +1,43 @@
|
||||||
<script>
|
<script>
|
||||||
import { tables } from "stores/builder"
|
import { tables } from "stores/builder"
|
||||||
import { Select, Checkbox, Label } from "@budibase/bbui"
|
import {
|
||||||
|
ActionButton,
|
||||||
|
Popover,
|
||||||
|
Icon,
|
||||||
|
TooltipPosition,
|
||||||
|
TooltipType,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { FieldType } from "@budibase/types"
|
import { FieldType } from "@budibase/types"
|
||||||
|
|
||||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||||
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
import { TableNames } from "constants"
|
import { FIELDS } from "constants/backend"
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
import { memo } from "@budibase/frontend-core"
|
||||||
|
import PropField from "./PropField.svelte"
|
||||||
|
import { cloneDeep, isPlainObject, mergeWith } from "lodash"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
export let value
|
|
||||||
|
export let row
|
||||||
export let meta
|
export let meta
|
||||||
export let bindings
|
export let bindings
|
||||||
export let isTestModal
|
export let isTestModal
|
||||||
export let isUpdateRow
|
|
||||||
|
|
||||||
$: parsedBindings = bindings.map(binding => {
|
const typeToField = Object.values(FIELDS).reduce((acc, field) => {
|
||||||
let clone = Object.assign({}, binding)
|
acc[field.type] = field
|
||||||
clone.icon = "ShareAndroid"
|
return acc
|
||||||
return clone
|
}, {})
|
||||||
|
|
||||||
|
const memoStore = memo({
|
||||||
|
row,
|
||||||
|
meta,
|
||||||
})
|
})
|
||||||
|
|
||||||
let table
|
let table
|
||||||
|
// Row Schema Fields
|
||||||
let schemaFields
|
let schemaFields
|
||||||
let attachmentTypes = [
|
let attachmentTypes = [
|
||||||
FieldType.ATTACHMENTS,
|
FieldType.ATTACHMENTS,
|
||||||
|
@ -30,32 +45,123 @@
|
||||||
FieldType.SIGNATURE_SINGLE,
|
FieldType.SIGNATURE_SINGLE,
|
||||||
]
|
]
|
||||||
|
|
||||||
$: {
|
let customPopover
|
||||||
table = $tables.list.find(table => table._id === value?.tableId)
|
let popoverAnchor
|
||||||
|
let editableRow = {}
|
||||||
|
let editableFields = {}
|
||||||
|
|
||||||
// Just sorting attachment types to the bottom here for a cleaner UX
|
// Avoid unnecessary updates
|
||||||
schemaFields = Object.entries(table?.schema ?? {}).sort(
|
$: memoStore.set({
|
||||||
([, schemaA], [, schemaB]) =>
|
row,
|
||||||
(schemaA.type === "attachment") - (schemaB.type === "attachment")
|
meta,
|
||||||
|
})
|
||||||
|
|
||||||
|
$: parsedBindings = bindings.map(binding => {
|
||||||
|
let clone = Object.assign({}, binding)
|
||||||
|
clone.icon = "ShareAndroid"
|
||||||
|
return clone
|
||||||
|
})
|
||||||
|
|
||||||
|
$: tableId = $memoStore?.row?.tableId
|
||||||
|
|
||||||
|
$: initData(tableId, $memoStore?.meta?.fields, $memoStore?.row)
|
||||||
|
|
||||||
|
const initData = (tableId, metaFields, row) => {
|
||||||
|
if (!tableId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refesh the editable fields
|
||||||
|
editableFields = cloneDeep(metaFields || {})
|
||||||
|
|
||||||
|
// Refresh all the row data
|
||||||
|
editableRow = cloneDeep(row || {})
|
||||||
|
|
||||||
|
table = $tables.list.find(table => table._id === tableId)
|
||||||
|
|
||||||
|
if (table) {
|
||||||
|
editableRow["tableId"] = tableId
|
||||||
|
|
||||||
|
schemaFields = Object.entries(table?.schema ?? {})
|
||||||
|
.filter(entry => {
|
||||||
|
const [, field] = entry
|
||||||
|
return field.type !== "formula" && !field.autocolumn
|
||||||
|
})
|
||||||
|
.sort(([nameA], [nameB]) => {
|
||||||
|
return nameA < nameB ? -1 : 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// Parse out any data not in the schema.
|
||||||
|
for (const column in editableFields) {
|
||||||
|
if (!Object.hasOwn(table?.schema, column)) {
|
||||||
|
delete editableFields[column]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go through the table schema and build out the editable content
|
||||||
|
for (const entry of schemaFields) {
|
||||||
|
const [key, fieldSchema] = entry
|
||||||
|
|
||||||
|
const emptyField =
|
||||||
|
editableRow[key] == null || editableRow[key]?.length === 0
|
||||||
|
|
||||||
|
// Put non-empty elements into the update and add their key to the fields list.
|
||||||
|
if (!emptyField && !Object.hasOwn(editableFields, key)) {
|
||||||
|
editableFields = {
|
||||||
|
...editableFields,
|
||||||
|
[key]: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy - clearRelationships
|
||||||
|
// Init the field and add it to the update.
|
||||||
|
if (emptyField) {
|
||||||
|
if (editableFields[key]?.clearRelationships === true) {
|
||||||
|
const emptyField = coerce(
|
||||||
|
!Object.hasOwn($memoStore?.row, key) ? "" : $memoStore?.row[key],
|
||||||
|
fieldSchema.type
|
||||||
)
|
)
|
||||||
|
|
||||||
schemaFields.forEach(([, schema]) => {
|
// remove this and place the field in the editable row.
|
||||||
if (!schema.autocolumn && !value[schema.name]) {
|
delete editableFields[key]?.clearRelationships
|
||||||
value[schema.name] = ""
|
|
||||||
|
// Default the field
|
||||||
|
editableRow = {
|
||||||
|
...editableRow,
|
||||||
|
[key]: emptyField,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Purge from the update as its presence is not necessary.
|
||||||
|
delete editableRow[key]
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const onChangeTable = e => {
|
|
||||||
value["tableId"] = e.detail
|
|
||||||
dispatch("change", value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse all known row schema keys
|
||||||
|
const schemaKeys = [
|
||||||
|
"tableId",
|
||||||
|
...schemaFields.map(entry => {
|
||||||
|
const [key] = entry
|
||||||
|
return key
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
// Purge any row keys that are not present in the schema.
|
||||||
|
for (const rowKey of Object.keys(editableRow)) {
|
||||||
|
if (!schemaKeys.includes(rowKey)) {
|
||||||
|
delete editableRow[rowKey]
|
||||||
|
delete editableFields[rowKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row coerce
|
||||||
const coerce = (value, type) => {
|
const coerce = (value, type) => {
|
||||||
const re = new RegExp(/{{([^{].*?)}}/g)
|
const re = new RegExp(/{{([^{].*?)}}/g)
|
||||||
if (re.test(value)) {
|
if (typeof value === "string" && re.test(value)) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "number") {
|
if (type === "number") {
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
return value
|
return value
|
||||||
|
@ -66,6 +172,9 @@
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
if (type === "array") {
|
if (type === "array") {
|
||||||
|
if (!value) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
@ -73,7 +182,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "link") {
|
if (type === "link") {
|
||||||
if (Array.isArray(value)) {
|
if (!value) {
|
||||||
|
return []
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
return value.split(",").map(x => x.trim())
|
return value.split(",").map(x => x.trim())
|
||||||
|
@ -86,70 +197,73 @@
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChange = (e, field, type) => {
|
const isFullWidth = type => {
|
||||||
let newValue = {
|
return (
|
||||||
...value,
|
attachmentTypes.includes(type) ||
|
||||||
[field]: coerce(e.detail, type),
|
type === FieldType.JSON ||
|
||||||
}
|
type === FieldType.LONGFORM
|
||||||
dispatch("change", newValue)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChangeSetting = (field, key, value) => {
|
const onChange = update => {
|
||||||
let newField = {}
|
const customizer = (objValue, srcValue) => {
|
||||||
newField[field] = {
|
if (isPlainObject(objValue) && isPlainObject(srcValue)) {
|
||||||
[key]: value,
|
const result = mergeWith({}, objValue, srcValue, customizer)
|
||||||
|
let outcome = Object.keys(result).reduce((acc, key) => {
|
||||||
|
if (result[key] !== null) {
|
||||||
|
acc[key] = result[key]
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
return outcome
|
||||||
|
}
|
||||||
|
return srcValue
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedFields = {
|
const result = mergeWith(
|
||||||
...meta?.fields,
|
{},
|
||||||
...newField,
|
{
|
||||||
|
row: editableRow,
|
||||||
|
meta: {
|
||||||
|
fields: editableFields,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update,
|
||||||
|
customizer
|
||||||
|
)
|
||||||
|
dispatch("change", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch("change", {
|
|
||||||
key: "meta",
|
|
||||||
fields: updatedFields,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Ensure any nullish tableId values get set to empty string so
|
|
||||||
// that the select works
|
|
||||||
$: if (value?.tableId == null) value = { tableId: "" }
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="schema-fields">
|
{#each schemaFields || [] as [field, schema]}
|
||||||
<Label>Table</Label>
|
{#if !schema.autocolumn && Object.hasOwn(editableFields, field)}
|
||||||
<div class="field-width">
|
<PropField label={field} fullWidth={isFullWidth(schema.type)}>
|
||||||
<Select
|
<div class="prop-control-wrap">
|
||||||
on:change={onChangeTable}
|
|
||||||
value={value.tableId}
|
|
||||||
options={$tables.list.filter(table => table._id !== TableNames.USERS)}
|
|
||||||
getOptionLabel={table => table.name}
|
|
||||||
getOptionValue={table => table._id}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if schemaFields.length}
|
|
||||||
{#each schemaFields as [field, schema]}
|
|
||||||
{#if !schema.autocolumn}
|
|
||||||
<div class:schema-fields={!attachmentTypes.includes(schema.type)}>
|
|
||||||
<Label>{field}</Label>
|
|
||||||
<div class:field-width={!attachmentTypes.includes(schema.type)}>
|
|
||||||
{#if isTestModal}
|
{#if isTestModal}
|
||||||
<RowSelectorTypes
|
<RowSelectorTypes
|
||||||
{isTestModal}
|
{isTestModal}
|
||||||
{field}
|
{field}
|
||||||
{schema}
|
{schema}
|
||||||
bindings={parsedBindings}
|
bindings={parsedBindings}
|
||||||
{value}
|
value={editableRow}
|
||||||
|
meta={{
|
||||||
|
fields: editableFields,
|
||||||
|
}}
|
||||||
{onChange}
|
{onChange}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<DrawerBindableSlot
|
<DrawerBindableSlot
|
||||||
title={value.title || field}
|
title={$memoStore?.row?.title || field}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type={schema.type}
|
type={schema.type}
|
||||||
{schema}
|
{schema}
|
||||||
value={value[field]}
|
value={editableRow[field]}
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e =>
|
||||||
|
onChange({
|
||||||
|
row: {
|
||||||
|
[field]: e.detail,
|
||||||
|
},
|
||||||
|
})}
|
||||||
{bindings}
|
{bindings}
|
||||||
allowJS={true}
|
allowJS={true}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
|
@ -160,56 +274,99 @@
|
||||||
{field}
|
{field}
|
||||||
{schema}
|
{schema}
|
||||||
bindings={parsedBindings}
|
bindings={parsedBindings}
|
||||||
{value}
|
value={editableRow}
|
||||||
{onChange}
|
meta={{
|
||||||
useAttachmentBinding={meta?.fields?.[field]
|
fields: editableFields,
|
||||||
?.useAttachmentBinding}
|
}}
|
||||||
{onChangeSetting}
|
onChange={change => onChange(change)}
|
||||||
/>
|
/>
|
||||||
</DrawerBindableSlot>
|
</DrawerBindableSlot>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
</PropField>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
{#if isUpdateRow && schema.type === "link"}
|
{#if table && schemaFields}
|
||||||
<div class="checkbox-field">
|
{#key editableFields}
|
||||||
<Checkbox
|
<div
|
||||||
value={meta.fields?.[field]?.clearRelationships}
|
class="add-fields-btn"
|
||||||
text={"Clear relationships if empty?"}
|
class:empty={Object.is(editableFields, {})}
|
||||||
size={"S"}
|
bind:this={popoverAnchor}
|
||||||
on:change={e =>
|
>
|
||||||
onChangeSetting(field, "clearRelationships", e.detail)}
|
<ActionButton
|
||||||
/>
|
icon="Add"
|
||||||
|
fullWidth
|
||||||
|
on:click={() => {
|
||||||
|
customPopover.show()
|
||||||
|
}}
|
||||||
|
disabled={!schemaFields}
|
||||||
|
>Add fields
|
||||||
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/key}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
align="center"
|
||||||
|
bind:this={customPopover}
|
||||||
|
anchor={popoverAnchor}
|
||||||
|
useAnchorWidth
|
||||||
|
maxHeight={300}
|
||||||
|
resizable={false}
|
||||||
|
offset={10}
|
||||||
|
>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
|
<ul class="spectrum-Menu" role="listbox">
|
||||||
|
{#each schemaFields || [] as [field, schema]}
|
||||||
|
{#if !schema.autocolumn}
|
||||||
|
<li
|
||||||
|
class="table_field spectrum-Menu-item"
|
||||||
|
class:is-selected={Object.hasOwn(editableFields, field)}
|
||||||
|
on:click={() => {
|
||||||
|
if (Object.hasOwn(editableFields, field)) {
|
||||||
|
delete editableFields[field]
|
||||||
|
onChange({
|
||||||
|
meta: { fields: editableFields },
|
||||||
|
row: { [field]: null },
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
editableFields[field] = {}
|
||||||
|
onChange({ meta: { fields: editableFields } })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={typeToField?.[schema.type]?.icon}
|
||||||
|
color={"var(--spectrum-global-color-gray-600)"}
|
||||||
|
tooltip={capitalise(schema.type)}
|
||||||
|
tooltipType={TooltipType.Info}
|
||||||
|
tooltipPosition={TooltipPosition.Left}
|
||||||
|
/>
|
||||||
|
<div class="field_name spectrum-Menu-itemLabel">{field}</div>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.field-width {
|
.table_field {
|
||||||
width: 320px;
|
display: flex;
|
||||||
|
padding: var(--spacing-s) var(--spacing-l);
|
||||||
|
gap: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.schema-fields {
|
/* Override for general json field override */
|
||||||
display: flex;
|
.prop-control-wrap :global(.icon.json-slot-icon) {
|
||||||
justify-content: space-between;
|
right: 1px !important;
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
flex: 1;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.schema-fields :global(label) {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
.checkbox-field {
|
|
||||||
padding-bottom: var(--spacing-s);
|
|
||||||
padding-left: 1px;
|
|
||||||
padding-top: var(--spacing-s);
|
|
||||||
}
|
|
||||||
.checkbox-field :global(label) {
|
|
||||||
text-transform: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -11,17 +11,18 @@
|
||||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
import Editor from "components/integration/QueryEditor.svelte"
|
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
|
|
||||||
export let onChange
|
export let onChange
|
||||||
export let field
|
export let field
|
||||||
export let schema
|
export let schema
|
||||||
export let value
|
export let value
|
||||||
|
export let meta
|
||||||
export let bindings
|
export let bindings
|
||||||
export let isTestModal
|
export let isTestModal
|
||||||
export let useAttachmentBinding
|
|
||||||
export let onChangeSetting
|
$: fieldData = value[field]
|
||||||
|
|
||||||
$: parsedBindings = bindings.map(binding => {
|
$: parsedBindings = bindings.map(binding => {
|
||||||
let clone = Object.assign({}, binding)
|
let clone = Object.assign({}, binding)
|
||||||
|
@ -35,14 +36,15 @@
|
||||||
FieldType.SIGNATURE_SINGLE,
|
FieldType.SIGNATURE_SINGLE,
|
||||||
]
|
]
|
||||||
|
|
||||||
let previousBindingState = useAttachmentBinding
|
|
||||||
|
|
||||||
function schemaHasOptions(schema) {
|
function schemaHasOptions(schema) {
|
||||||
return !!schema.constraints?.inclusion?.length
|
return !!schema.constraints?.inclusion?.length
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAttachmentParams(keyValueObj) {
|
function handleAttachmentParams(keyValueObj) {
|
||||||
let params = {}
|
let params = {}
|
||||||
|
if (!keyValueObj) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (!Array.isArray(keyValueObj) && keyValueObj) {
|
if (!Array.isArray(keyValueObj) && keyValueObj) {
|
||||||
keyValueObj = [keyValueObj]
|
keyValueObj = [keyValueObj]
|
||||||
|
@ -50,45 +52,68 @@
|
||||||
|
|
||||||
if (keyValueObj.length) {
|
if (keyValueObj.length) {
|
||||||
for (let param of keyValueObj) {
|
for (let param of keyValueObj) {
|
||||||
params[param.url] = param.filename
|
params[param.url || ""] = param.filename || ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleToggleChange(toggleField, event) {
|
const handleMediaUpdate = e => {
|
||||||
if (event.detail === true) {
|
const media = e.detail || []
|
||||||
value[toggleField] = []
|
const isSingle =
|
||||||
} else {
|
schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
value[toggleField] = ""
|
schema.type === FieldType.SIGNATURE_SINGLE
|
||||||
|
const parsedMedia = media.map(({ name, value }) => ({
|
||||||
|
url: name,
|
||||||
|
filename: value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (isSingle) {
|
||||||
|
const [singleMedia] = parsedMedia
|
||||||
|
// Return only the first entry
|
||||||
|
return singleMedia
|
||||||
|
? {
|
||||||
|
url: singleMedia.url,
|
||||||
|
filename: singleMedia.filename,
|
||||||
}
|
}
|
||||||
previousBindingState = event.detail
|
: null
|
||||||
onChangeSetting(toggleField, "useAttachmentBinding", event.detail)
|
|
||||||
onChange({ detail: value[toggleField] }, toggleField)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (useAttachmentBinding !== previousBindingState) {
|
// Return the entire array
|
||||||
if (useAttachmentBinding) {
|
return parsedMedia
|
||||||
value[field] = []
|
|
||||||
} else {
|
|
||||||
value[field] = ""
|
|
||||||
}
|
|
||||||
previousBindingState = useAttachmentBinding
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if schemaHasOptions(schema) && schema.type !== "array"}
|
{#if schemaHasOptions(schema) && schema.type !== "array"}
|
||||||
<Select
|
<Select
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e =>
|
||||||
value={value[field]}
|
onChange({
|
||||||
|
row: {
|
||||||
|
[field]: e.detail,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
value={fieldData}
|
||||||
options={schema.constraints.inclusion}
|
options={schema.constraints.inclusion}
|
||||||
/>
|
/>
|
||||||
{:else if schema.type === "datetime"}
|
{:else if schema.type === "datetime"}
|
||||||
<DatePicker value={value[field]} on:change={e => onChange(e, field)} />
|
<DatePicker
|
||||||
|
value={fieldData}
|
||||||
|
on:change={e =>
|
||||||
|
onChange({
|
||||||
|
row: {
|
||||||
|
[field]: e.detail,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
{:else if schema.type === "boolean"}
|
{:else if schema.type === "boolean"}
|
||||||
<Select
|
<Select
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e =>
|
||||||
value={value[field]}
|
onChange({
|
||||||
|
row: {
|
||||||
|
[field]: e.detail,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
value={fieldData}
|
||||||
options={[
|
options={[
|
||||||
{ label: "True", value: "true" },
|
{ label: "True", value: "true" },
|
||||||
{ label: "False", value: "false" },
|
{ label: "False", value: "false" },
|
||||||
|
@ -96,83 +121,111 @@
|
||||||
/>
|
/>
|
||||||
{:else if schemaHasOptions(schema) && schema.type === "array"}
|
{:else if schemaHasOptions(schema) && schema.type === "array"}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
bind:value={value[field]}
|
value={fieldData}
|
||||||
options={schema.constraints.inclusion}
|
options={schema.constraints.inclusion}
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e =>
|
||||||
|
onChange({
|
||||||
|
row: {
|
||||||
|
[field]: e.detail,
|
||||||
|
},
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
{:else if schema.type === "longform"}
|
{:else if schema.type === "longform"}
|
||||||
<TextArea bind:value={value[field]} on:change={e => onChange(e, field)} />
|
<TextArea
|
||||||
|
value={fieldData}
|
||||||
|
on:change={e =>
|
||||||
|
onChange({
|
||||||
|
row: {
|
||||||
|
[field]: e.detail,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
{:else if schema.type === "json"}
|
{:else if schema.type === "json"}
|
||||||
<span>
|
<span>
|
||||||
<Editor
|
<div class="field-wrap json-field">
|
||||||
editorHeight="150"
|
<CodeEditor
|
||||||
mode="json"
|
value={fieldData}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
if (e.detail?.value !== value[field]) {
|
onChange({
|
||||||
onChange(e, field, schema.type)
|
row: {
|
||||||
}
|
[field]: e.detail,
|
||||||
|
},
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
value={value[field]}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
{:else if schema.type === "link"}
|
{:else if schema.type === "link"}
|
||||||
<LinkedRowSelector
|
<LinkedRowSelector
|
||||||
linkedRows={value[field]}
|
linkedRows={fieldData}
|
||||||
{schema}
|
{schema}
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e =>
|
||||||
|
onChange({
|
||||||
|
row: {
|
||||||
|
[field]: e.detail,
|
||||||
|
},
|
||||||
|
})}
|
||||||
useLabel={false}
|
useLabel={false}
|
||||||
/>
|
/>
|
||||||
{:else if schema.type === "bb_reference" || schema.type === "bb_reference_single"}
|
{:else if schema.type === "bb_reference" || schema.type === "bb_reference_single"}
|
||||||
<LinkedRowSelector
|
<LinkedRowSelector
|
||||||
linkedRows={value[field]}
|
linkedRows={fieldData}
|
||||||
{schema}
|
{schema}
|
||||||
linkedTableId={"ta_users"}
|
linkedTableId={"ta_users"}
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e =>
|
||||||
|
onChange({
|
||||||
|
row: {
|
||||||
|
[field]: e.detail,
|
||||||
|
},
|
||||||
|
})}
|
||||||
useLabel={false}
|
useLabel={false}
|
||||||
/>
|
/>
|
||||||
{:else if attachmentTypes.includes(schema.type)}
|
{:else if attachmentTypes.includes(schema.type)}
|
||||||
<div class="attachment-field-container">
|
<div class="attachment-field-container">
|
||||||
<div class="toggle-container">
|
<div class="toggle-container">
|
||||||
<Toggle
|
<Toggle
|
||||||
value={useAttachmentBinding}
|
value={meta?.fields?.[field]?.useAttachmentBinding}
|
||||||
text={"Use bindings"}
|
text={"Use bindings"}
|
||||||
size={"XS"}
|
size={"XS"}
|
||||||
on:change={e => handleToggleChange(field, e)}
|
on:change={e => {
|
||||||
|
onChange({
|
||||||
|
row: {
|
||||||
|
[field]: null,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
fields: {
|
||||||
|
[field]: {
|
||||||
|
useAttachmentBinding: e.detail,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if !useAttachmentBinding}
|
|
||||||
|
{#if !meta?.fields?.[field]?.useAttachmentBinding}
|
||||||
<div class="attachment-field-spacing">
|
<div class="attachment-field-spacing">
|
||||||
<KeyValueBuilder
|
<KeyValueBuilder
|
||||||
on:change={async e => {
|
on:change={e => {
|
||||||
onChange(
|
onChange({
|
||||||
{
|
row: {
|
||||||
detail:
|
[field]: handleMediaUpdate(e),
|
||||||
schema.type === FieldType.ATTACHMENT_SINGLE ||
|
|
||||||
schema.type === FieldType.SIGNATURE_SINGLE
|
|
||||||
? e.detail.length > 0
|
|
||||||
? {
|
|
||||||
url: e.detail[0].name,
|
|
||||||
filename: e.detail[0].value,
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
: e.detail.map(({ name, value }) => ({
|
|
||||||
url: name,
|
|
||||||
filename: value,
|
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
field
|
})
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
object={handleAttachmentParams(value[field])}
|
object={handleAttachmentParams(fieldData)}
|
||||||
allowJS
|
allowJS
|
||||||
{bindings}
|
{bindings}
|
||||||
keyBindings
|
keyBindings
|
||||||
customButtonText={"Add attachment"}
|
customButtonText={schema.type === FieldType.SIGNATURE_SINGLE
|
||||||
|
? "Add signature"
|
||||||
|
: "Add attachment"}
|
||||||
keyPlaceholder={"URL"}
|
keyPlaceholder={"URL"}
|
||||||
valuePlaceholder={"Filename"}
|
valuePlaceholder={"Filename"}
|
||||||
actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE ||
|
actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
schema.type === FieldType.SIGNATURE) &&
|
schema.type === FieldType.SIGNATURE_SINGLE) &&
|
||||||
Object.keys(value[field]).length >= 1}
|
fieldData}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -180,8 +233,13 @@
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
value={value[field]}
|
value={fieldData}
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e =>
|
||||||
|
onChange({
|
||||||
|
row: {
|
||||||
|
[field]: e.detail,
|
||||||
|
},
|
||||||
|
})}
|
||||||
type="string"
|
type="string"
|
||||||
bindings={parsedBindings}
|
bindings={parsedBindings}
|
||||||
allowJS={true}
|
allowJS={true}
|
||||||
|
@ -195,20 +253,41 @@
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
value={value[field]}
|
value={fieldData}
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e =>
|
||||||
|
onChange({
|
||||||
|
row: {
|
||||||
|
[field]: e.detail,
|
||||||
|
},
|
||||||
|
})}
|
||||||
type="string"
|
type="string"
|
||||||
bindings={parsedBindings}
|
bindings={parsedBindings}
|
||||||
allowJS={true}
|
allowJS={true}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
title={schema.name}
|
title={schema.name}
|
||||||
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.attachment-field-spacing,
|
.attachment-field-spacing {
|
||||||
.json-input-spacing {
|
border: 1px solid var(--spectrum-global-color-gray-400);
|
||||||
margin-top: var(--spacing-s);
|
border-radius: 4px;
|
||||||
margin-bottom: var(--spacing-l);
|
padding: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-wrap.json-field {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-wrap {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-400);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-wrap :global(.cm-editor),
|
||||||
|
.field-wrap :global(.cm-scroller) {
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let isTrigger
|
export let isTrigger
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
$: filteredTables = $tables.list.filter(table => {
|
$: filteredTables = $tables.list.filter(table => {
|
||||||
return !isTrigger || table._id !== TableNames.USERS
|
return !isTrigger || table._id !== TableNames.USERS
|
||||||
|
@ -25,4 +26,5 @@
|
||||||
options={filteredTables}
|
options={filteredTables}
|
||||||
getOptionLabel={table => table.name}
|
getOptionLabel={table => table.name}
|
||||||
getOptionValue={table => table._id}
|
getOptionValue={table => table._id}
|
||||||
|
{disabled}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
export let disableBindings = false
|
export let disableBindings = false
|
||||||
export let forceModal = false
|
export let forceModal = false
|
||||||
export let context = null
|
export let context = null
|
||||||
|
export let autocomplete
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -71,6 +72,7 @@
|
||||||
on:blur={onBlur}
|
on:blur={onBlur}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{updateOnChange}
|
{updateOnChange}
|
||||||
|
{autocomplete}
|
||||||
/>
|
/>
|
||||||
{#if !disabled && !disableBindings}
|
{#if !disabled && !disableBindings}
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
export { default as BindableCombobox } from "./BindableCombobox.svelte"
|
||||||
|
export { default as BindingPanel } from "./BindingPanel.svelte"
|
||||||
|
export { default as BindingSidePanel } from "./BindingSidePanel.svelte"
|
||||||
|
export { default as DrawerBindableCombobox } from "./DrawerBindableCombobox.svelte"
|
||||||
|
export { default as ClientBindingPanel } from "./ClientBindingPanel.svelte"
|
||||||
|
export { default as DrawerBindableInput } from "./DrawerBindableInput.svelte"
|
||||||
|
export { default as DrawerBindableSlot } from "./DrawerBindableSlot.svelte"
|
||||||
|
export { default as EvaluationSidePanel } from "./EvaluationSidePanel.svelte"
|
||||||
|
export { default as ModalBindableInput } from "./ModalBindableInput.svelte"
|
||||||
|
export { default as ServerBindingPanel } from "./ServerBindingPanel.svelte"
|
||||||
|
export { default as SnippetDrawer } from "./SnippetDrawer.svelte"
|
||||||
|
export { default as SnippetSidePanel } from "./SnippetSidePanel.svelte"
|
|
@ -11,7 +11,7 @@
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { AUTH_TYPE_LABELS, AUTH_TYPES } from "./authTypes"
|
import { AUTH_TYPE_LABELS, AUTH_TYPES } from "./authTypes"
|
||||||
import BindableCombobox from "components/common/bindings/BindableCombobox.svelte"
|
import { BindableCombobox } from "components/common/bindings"
|
||||||
import { getAuthBindings, getEnvironmentBindings } from "dataBinding"
|
import { getAuthBindings, getEnvironmentBindings } from "dataBinding"
|
||||||
import { environment, licensing, auth } from "stores/portal"
|
import { environment, licensing, auth } from "stores/portal"
|
||||||
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
||||||
|
|
|
@ -157,7 +157,8 @@ const automationActions = store => ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateBlockInputs: async (block, data) => {
|
|
||||||
|
processBlockInputs: async (block, data) => {
|
||||||
// Create new modified block
|
// Create new modified block
|
||||||
let newBlock = {
|
let newBlock = {
|
||||||
...block,
|
...block,
|
||||||
|
@ -184,6 +185,14 @@ const automationActions = store => ({
|
||||||
|
|
||||||
// Don't save if no changes were made
|
// Don't save if no changes were made
|
||||||
if (JSON.stringify(newAutomation) === JSON.stringify(automation)) {
|
if (JSON.stringify(newAutomation) === JSON.stringify(automation)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return newAutomation
|
||||||
|
},
|
||||||
|
updateBlockInputs: async (block, data) => {
|
||||||
|
const newAutomation = await store.actions.processBlockInputs(block, data)
|
||||||
|
if (newAutomation === false) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await store.actions.save(newAutomation)
|
await store.actions.save(newAutomation)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit e8f2c5a14780e1f61ec3896821ba5f93d486eb72
|
Subproject commit dbb78c8737c291871500bc655e30f331f6ffccbf
|
|
@ -1,23 +0,0 @@
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
module SendgridMock {
|
|
||||||
class Email {
|
|
||||||
constructor() {
|
|
||||||
// @ts-ignore
|
|
||||||
this.apiKey = null
|
|
||||||
}
|
|
||||||
|
|
||||||
setApiKey(apiKey: any) {
|
|
||||||
// @ts-ignore
|
|
||||||
this.apiKey = apiKey
|
|
||||||
}
|
|
||||||
|
|
||||||
async send(msg: any) {
|
|
||||||
if (msg.to === "invalid@example.com") {
|
|
||||||
throw "Invalid"
|
|
||||||
}
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new Email()
|
|
||||||
}
|
|
|
@ -94,7 +94,7 @@
|
||||||
"koa2-ratelimit": "1.1.1",
|
"koa2-ratelimit": "1.1.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"memorystream": "0.3.1",
|
"memorystream": "0.3.1",
|
||||||
"mongodb": "^6.8.0",
|
"mongodb": "6.7.0",
|
||||||
"mssql": "10.0.1",
|
"mssql": "10.0.1",
|
||||||
"mysql2": "3.9.8",
|
"mysql2": "3.9.8",
|
||||||
"node-fetch": "2.6.7",
|
"node-fetch": "2.6.7",
|
||||||
|
|
|
@ -10,6 +10,6 @@ export const MIGRATIONS: AppMigration[] = [
|
||||||
{
|
{
|
||||||
id: "20240604153647_initial_sqs",
|
id: "20240604153647_initial_sqs",
|
||||||
func: m20240604153647_initial_sqs,
|
func: m20240604153647_initial_sqs,
|
||||||
disabled: !env.SQS_SEARCH_ENABLE,
|
disabled: !(env.SQS_MIGRATION_ENABLE || env.SQS_SEARCH_ENABLE),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -40,7 +40,7 @@ const migration = async () => {
|
||||||
// only do initial search if environment is using SQS already
|
// only do initial search if environment is using SQS already
|
||||||
// initial search makes sure that all the indexes have been created
|
// initial search makes sure that all the indexes have been created
|
||||||
// and are ready to use, avoiding any initial waits for large tables
|
// and are ready to use, avoiding any initial waits for large tables
|
||||||
if (env.SQS_SEARCH_ENABLE) {
|
if (env.SQS_MIGRATION_ENABLE || env.SQS_SEARCH_ENABLE) {
|
||||||
const tables = await sdk.tables.getAllInternalTables()
|
const tables = await sdk.tables.getAllInternalTables()
|
||||||
// do these one by one - running in parallel could cause problems
|
// do these one by one - running in parallel could cause problems
|
||||||
for (let table of tables) {
|
for (let table of tables) {
|
||||||
|
|
|
@ -66,16 +66,21 @@ function oldLinkDocument(): Omit<LinkDocument, "tableId"> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sqsDisabled(cb: () => Promise<void>) {
|
type SQSEnvVar = "SQS_MIGRATION_ENABLE" | "SQS_SEARCH_ENABLE"
|
||||||
await config.withEnv({ SQS_SEARCH_ENABLE: "" }, cb)
|
|
||||||
|
async function sqsDisabled(envVar: SQSEnvVar, cb: () => Promise<void>) {
|
||||||
|
await config.withEnv({ [envVar]: "" }, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sqsEnabled(cb: () => Promise<void>) {
|
async function sqsEnabled(envVar: SQSEnvVar, cb: () => Promise<void>) {
|
||||||
await config.withEnv({ SQS_SEARCH_ENABLE: "1" }, cb)
|
await config.withEnv({ [envVar]: "1" }, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeAll(async () => {
|
describe.each(["SQS_MIGRATION_ENABLE", "SQS_SEARCH_ENABLE"] as SQSEnvVar[])(
|
||||||
await sqsDisabled(async () => {
|
"SQS migration with (%s)",
|
||||||
|
envVar => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await sqsDisabled(envVar, async () => {
|
||||||
await config.init()
|
await config.init()
|
||||||
const table = await config.api.table.save(basicTable())
|
const table = await config.api.table.save(basicTable())
|
||||||
tableId = table._id!
|
tableId = table._id!
|
||||||
|
@ -83,13 +88,12 @@ beforeAll(async () => {
|
||||||
// old link document
|
// old link document
|
||||||
await db.put(oldLinkDocument())
|
await db.put(oldLinkDocument())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("SQS migration", () => {
|
|
||||||
it("test migration runs as expected against an older DB", async () => {
|
it("test migration runs as expected against an older DB", async () => {
|
||||||
const db = dbCore.getDB(config.appId!)
|
const db = dbCore.getDB(config.appId!)
|
||||||
// confirm nothing exists initially
|
// confirm nothing exists initially
|
||||||
await sqsDisabled(async () => {
|
await sqsDisabled(envVar, async () => {
|
||||||
let error: any | undefined
|
let error: any | undefined
|
||||||
try {
|
try {
|
||||||
await db.get(SQLITE_DESIGN_DOC_ID)
|
await db.get(SQLITE_DESIGN_DOC_ID)
|
||||||
|
@ -99,7 +103,7 @@ describe("SQS migration", () => {
|
||||||
expect(error).toBeDefined()
|
expect(error).toBeDefined()
|
||||||
expect(error.status).toBe(404)
|
expect(error.status).toBe(404)
|
||||||
})
|
})
|
||||||
await sqsEnabled(async () => {
|
await sqsEnabled(envVar, async () => {
|
||||||
await processMigrations(config.appId!, MIGRATIONS)
|
await processMigrations(config.appId!, MIGRATIONS)
|
||||||
const designDoc = await db.get<SQLiteDefinition>(SQLITE_DESIGN_DOC_ID)
|
const designDoc = await db.get<SQLiteDefinition>(SQLITE_DESIGN_DOC_ID)
|
||||||
expect(designDoc.sql.tables).toBeDefined()
|
expect(designDoc.sql.tables).toBeDefined()
|
||||||
|
@ -126,4 +130,5 @@ describe("SQS migration", () => {
|
||||||
expect(linkDoc.doc2.rowId).toEqual(rowId1)
|
expect(linkDoc.doc2.rowId).toEqual(rowId1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -100,7 +100,10 @@ export function getError(err: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function guardAttachment(attachmentObject: any) {
|
export function guardAttachment(attachmentObject: any) {
|
||||||
if (!("url" in attachmentObject) || !("filename" in attachmentObject)) {
|
if (
|
||||||
|
attachmentObject &&
|
||||||
|
(!("url" in attachmentObject) || !("filename" in attachmentObject))
|
||||||
|
) {
|
||||||
const providedKeys = Object.keys(attachmentObject).join(", ")
|
const providedKeys = Object.keys(attachmentObject).join(", ")
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Attachments must have both "url" and "filename" keys. You have provided: ${providedKeys}`
|
`Attachments must have both "url" and "filename" keys. You have provided: ${providedKeys}`
|
||||||
|
@ -135,7 +138,9 @@ export async function sendAutomationAttachmentsToStorage(
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [prop, attachments] of Object.entries(attachmentRows)) {
|
for (const [prop, attachments] of Object.entries(attachmentRows)) {
|
||||||
if (Array.isArray(attachments)) {
|
if (!attachments) {
|
||||||
|
continue
|
||||||
|
} else if (Array.isArray(attachments)) {
|
||||||
if (attachments.length) {
|
if (attachments.length) {
|
||||||
row[prop] = await Promise.all(
|
row[prop] = await Promise.all(
|
||||||
attachments.map(attachment => generateAttachmentRow(attachment))
|
attachments.map(attachment => generateAttachmentRow(attachment))
|
||||||
|
|
|
@ -7,8 +7,8 @@ import {
|
||||||
AutomationStepType,
|
AutomationStepType,
|
||||||
AutomationIOType,
|
AutomationIOType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import { env } from "@budibase/backend-core"
|
||||||
import * as automationUtils from "../automationUtils"
|
import * as automationUtils from "../automationUtils"
|
||||||
import environment from "../../environment"
|
|
||||||
|
|
||||||
enum Model {
|
enum Model {
|
||||||
GPT_35_TURBO = "gpt-3.5-turbo",
|
GPT_35_TURBO = "gpt-3.5-turbo",
|
||||||
|
@ -60,7 +60,7 @@ export const definition: AutomationStepSchema = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run({ inputs }: AutomationStepInput) {
|
export async function run({ inputs }: AutomationStepInput) {
|
||||||
if (!environment.OPENAI_API_KEY) {
|
if (!env.OPENAI_API_KEY) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
response:
|
response:
|
||||||
|
@ -77,7 +77,7 @@ export async function run({ inputs }: AutomationStepInput) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const openai = new OpenAI({
|
const openai = new OpenAI({
|
||||||
apiKey: environment.OPENAI_API_KEY,
|
apiKey: env.OPENAI_API_KEY,
|
||||||
})
|
})
|
||||||
|
|
||||||
const completion = await openai.chat.completions.create({
|
const completion = await openai.chat.completions.create({
|
||||||
|
|
|
@ -82,39 +82,73 @@ export async function run({ inputs, appId, emitter }: AutomationStepInput) {
|
||||||
}
|
}
|
||||||
const tableId = inputs.row.tableId
|
const tableId = inputs.row.tableId
|
||||||
|
|
||||||
// clear any undefined, null or empty string properties so that they aren't updated
|
// Base update
|
||||||
for (let propKey of Object.keys(inputs.row)) {
|
let rowUpdate: Record<string, any>
|
||||||
const clearRelationships =
|
|
||||||
inputs.meta?.fields?.[propKey]?.clearRelationships
|
// Legacy
|
||||||
|
// Find previously set values and add them to the update. Ensure empty relationships
|
||||||
|
// are added to the update if clearRelationships is true
|
||||||
|
const legacyUpdated = Object.keys(inputs.row || {}).reduce(
|
||||||
|
(acc: Record<string, any>, key: string) => {
|
||||||
|
const isEmpty = inputs.row[key] == null || inputs.row[key]?.length === 0
|
||||||
|
const fieldConfig = inputs.meta?.fields || {}
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
if (
|
if (
|
||||||
(inputs.row[propKey] == null || inputs.row[propKey]?.length === 0) &&
|
Object.hasOwn(fieldConfig, key) &&
|
||||||
!clearRelationships
|
fieldConfig[key].clearRelationships === true
|
||||||
) {
|
) {
|
||||||
delete inputs.row[propKey]
|
// Explicitly clear the field on update
|
||||||
|
acc[key] = []
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Keep non-empty values
|
||||||
|
acc[key] = inputs.row[key]
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// The source of truth for inclusion in the update is: inputs.meta?.fields
|
||||||
|
const parsedUpdate = Object.keys(inputs.meta?.fields || {}).reduce(
|
||||||
|
(acc: Record<string, any>, key: string) => {
|
||||||
|
const fieldConfig = inputs.meta?.fields?.[key] || {}
|
||||||
|
// Ignore legacy config.
|
||||||
|
if (Object.hasOwn(fieldConfig, "clearRelationships")) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
acc[key] =
|
||||||
|
!inputs.row[key] || inputs.row[key]?.length === 0 ? "" : inputs.row[key]
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
rowUpdate = {
|
||||||
|
tableId,
|
||||||
|
...parsedUpdate,
|
||||||
|
...legacyUpdated,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (tableId) {
|
if (tableId) {
|
||||||
inputs.row = await automationUtils.cleanUpRow(
|
rowUpdate = await automationUtils.cleanUpRow(tableId, rowUpdate)
|
||||||
inputs.row.tableId,
|
|
||||||
inputs.row
|
|
||||||
)
|
|
||||||
|
|
||||||
inputs.row = await automationUtils.sendAutomationAttachmentsToStorage(
|
rowUpdate = await automationUtils.sendAutomationAttachmentsToStorage(
|
||||||
inputs.row.tableId,
|
tableId,
|
||||||
inputs.row
|
rowUpdate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// have to clean up the row, remove the table from it
|
// have to clean up the row, remove the table from it
|
||||||
const ctx: any = buildCtx(appId, emitter, {
|
const ctx: any = buildCtx(appId, emitter, {
|
||||||
body: {
|
body: {
|
||||||
...inputs.row,
|
...rowUpdate,
|
||||||
_id: inputs.rowId,
|
_id: inputs.rowId,
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
rowId: inputs.rowId,
|
rowId: inputs.rowId,
|
||||||
tableId: tableId,
|
tableId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await rowController.patch(ctx)
|
await rowController.patch(ctx)
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
const setup = require("./utilities")
|
import { getConfig, runStep, afterAll as _afterAll } from "./utilities"
|
||||||
|
|
||||||
import environment from "../../environment"
|
|
||||||
import { OpenAI } from "openai"
|
import { OpenAI } from "openai"
|
||||||
|
|
||||||
jest.mock("openai", () => ({
|
jest.mock("openai", () => ({
|
||||||
|
@ -26,42 +24,41 @@ const mockedOpenAI = OpenAI as jest.MockedClass<typeof OpenAI>
|
||||||
const OPENAI_PROMPT = "What is the meaning of life?"
|
const OPENAI_PROMPT = "What is the meaning of life?"
|
||||||
|
|
||||||
describe("test the openai action", () => {
|
describe("test the openai action", () => {
|
||||||
let config = setup.getConfig()
|
let config = getConfig()
|
||||||
|
let resetEnv: () => void | undefined
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await config.init()
|
await config.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
environment.OPENAI_API_KEY = "abc123"
|
resetEnv = config.setCoreEnv({ OPENAI_API_KEY: "abc123" })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterEach(() => {
|
||||||
|
resetEnv()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(_afterAll)
|
||||||
|
|
||||||
it("should present the correct error message when the OPENAI_API_KEY variable isn't set", async () => {
|
it("should present the correct error message when the OPENAI_API_KEY variable isn't set", async () => {
|
||||||
delete environment.OPENAI_API_KEY
|
await config.withCoreEnv({ OPENAI_API_KEY: "" }, async () => {
|
||||||
|
let res = await runStep("OPENAI", { prompt: OPENAI_PROMPT })
|
||||||
let res = await setup.runStep("OPENAI", {
|
|
||||||
prompt: OPENAI_PROMPT,
|
|
||||||
})
|
|
||||||
expect(res.response).toEqual(
|
expect(res.response).toEqual(
|
||||||
"OpenAI API Key not configured - please add the OPENAI_API_KEY environment variable."
|
"OpenAI API Key not configured - please add the OPENAI_API_KEY environment variable."
|
||||||
)
|
)
|
||||||
expect(res.success).toBeFalsy()
|
expect(res.success).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("should be able to receive a response from ChatGPT given a prompt", async () => {
|
it("should be able to receive a response from ChatGPT given a prompt", async () => {
|
||||||
const res = await setup.runStep("OPENAI", {
|
const res = await runStep("OPENAI", { prompt: OPENAI_PROMPT })
|
||||||
prompt: OPENAI_PROMPT,
|
|
||||||
})
|
|
||||||
expect(res.response).toEqual("This is a test")
|
expect(res.response).toEqual("This is a test")
|
||||||
expect(res.success).toBeTruthy()
|
expect(res.success).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should present the correct error message when a prompt is not provided", async () => {
|
it("should present the correct error message when a prompt is not provided", async () => {
|
||||||
const res = await setup.runStep("OPENAI", {
|
const res = await runStep("OPENAI", { prompt: null })
|
||||||
prompt: null,
|
|
||||||
})
|
|
||||||
expect(res.response).toEqual(
|
expect(res.response).toEqual(
|
||||||
"Budibase OpenAI Automation Failed: No prompt supplied"
|
"Budibase OpenAI Automation Failed: No prompt supplied"
|
||||||
)
|
)
|
||||||
|
@ -84,7 +81,7 @@ describe("test the openai action", () => {
|
||||||
} as any)
|
} as any)
|
||||||
)
|
)
|
||||||
|
|
||||||
const res = await setup.runStep("OPENAI", {
|
const res = await runStep("OPENAI", {
|
||||||
prompt: OPENAI_PROMPT,
|
prompt: OPENAI_PROMPT,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,12 @@ import {
|
||||||
AutomationStepType,
|
AutomationStepType,
|
||||||
AutomationTriggerSchema,
|
AutomationTriggerSchema,
|
||||||
AutomationTriggerStepId,
|
AutomationTriggerStepId,
|
||||||
|
AutomationEventType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
export const definition: AutomationTriggerSchema = {
|
export const definition: AutomationTriggerSchema = {
|
||||||
name: "App Action",
|
name: "App Action",
|
||||||
event: "app:trigger",
|
event: AutomationEventType.APP_TRIGGER,
|
||||||
icon: "Apps",
|
icon: "Apps",
|
||||||
tagline: "Automation fired from the frontend",
|
tagline: "Automation fired from the frontend",
|
||||||
description: "Trigger an automation from an action inside your app",
|
description: "Trigger an automation from an action inside your app",
|
||||||
|
|
|
@ -4,11 +4,12 @@ import {
|
||||||
AutomationStepType,
|
AutomationStepType,
|
||||||
AutomationTriggerSchema,
|
AutomationTriggerSchema,
|
||||||
AutomationTriggerStepId,
|
AutomationTriggerStepId,
|
||||||
|
AutomationEventType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
export const definition: AutomationTriggerSchema = {
|
export const definition: AutomationTriggerSchema = {
|
||||||
name: "Cron Trigger",
|
name: "Cron Trigger",
|
||||||
event: "cron:trigger",
|
event: AutomationEventType.CRON_TRIGGER,
|
||||||
icon: "Clock",
|
icon: "Clock",
|
||||||
tagline: "Cron Trigger (<b>{{inputs.cron}}</b>)",
|
tagline: "Cron Trigger (<b>{{inputs.cron}}</b>)",
|
||||||
description: "Triggers automation on a cron schedule.",
|
description: "Triggers automation on a cron schedule.",
|
||||||
|
|
|
@ -4,11 +4,12 @@ import {
|
||||||
AutomationStepType,
|
AutomationStepType,
|
||||||
AutomationTriggerSchema,
|
AutomationTriggerSchema,
|
||||||
AutomationTriggerStepId,
|
AutomationTriggerStepId,
|
||||||
|
AutomationEventType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
export const definition: AutomationTriggerSchema = {
|
export const definition: AutomationTriggerSchema = {
|
||||||
name: "Row Deleted",
|
name: "Row Deleted",
|
||||||
event: "row:delete",
|
event: AutomationEventType.ROW_DELETE,
|
||||||
icon: "TableRowRemoveCenter",
|
icon: "TableRowRemoveCenter",
|
||||||
tagline: "Row is deleted from {{inputs.enriched.table.name}}",
|
tagline: "Row is deleted from {{inputs.enriched.table.name}}",
|
||||||
description: "Fired when a row is deleted from your database",
|
description: "Fired when a row is deleted from your database",
|
||||||
|
|
|
@ -4,11 +4,12 @@ import {
|
||||||
AutomationStepType,
|
AutomationStepType,
|
||||||
AutomationTriggerSchema,
|
AutomationTriggerSchema,
|
||||||
AutomationTriggerStepId,
|
AutomationTriggerStepId,
|
||||||
|
AutomationEventType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
export const definition: AutomationTriggerSchema = {
|
export const definition: AutomationTriggerSchema = {
|
||||||
name: "Row Created",
|
name: "Row Created",
|
||||||
event: "row:save",
|
event: AutomationEventType.ROW_SAVE,
|
||||||
icon: "TableRowAddBottom",
|
icon: "TableRowAddBottom",
|
||||||
tagline: "Row is added to {{inputs.enriched.table.name}}",
|
tagline: "Row is added to {{inputs.enriched.table.name}}",
|
||||||
description: "Fired when a row is added to your database",
|
description: "Fired when a row is added to your database",
|
||||||
|
|
|
@ -4,11 +4,12 @@ import {
|
||||||
AutomationStepType,
|
AutomationStepType,
|
||||||
AutomationTriggerSchema,
|
AutomationTriggerSchema,
|
||||||
AutomationTriggerStepId,
|
AutomationTriggerStepId,
|
||||||
|
AutomationEventType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
export const definition: AutomationTriggerSchema = {
|
export const definition: AutomationTriggerSchema = {
|
||||||
name: "Row Updated",
|
name: "Row Updated",
|
||||||
event: "row:update",
|
event: AutomationEventType.ROW_UPDATE,
|
||||||
icon: "Refresh",
|
icon: "Refresh",
|
||||||
tagline: "Row is updated in {{inputs.enriched.table.name}}",
|
tagline: "Row is updated in {{inputs.enriched.table.name}}",
|
||||||
description: "Fired when a row is updated in your database",
|
description: "Fired when a row is updated in your database",
|
||||||
|
|
|
@ -4,11 +4,12 @@ import {
|
||||||
AutomationStepType,
|
AutomationStepType,
|
||||||
AutomationTriggerSchema,
|
AutomationTriggerSchema,
|
||||||
AutomationTriggerStepId,
|
AutomationTriggerStepId,
|
||||||
|
AutomationEventType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
export const definition: AutomationTriggerSchema = {
|
export const definition: AutomationTriggerSchema = {
|
||||||
name: "Webhook",
|
name: "Webhook",
|
||||||
event: "web:trigger",
|
event: AutomationEventType.WEBHOOK_TRIGGER,
|
||||||
icon: "Send",
|
icon: "Send",
|
||||||
tagline: "Webhook endpoint is hit",
|
tagline: "Webhook endpoint is hit",
|
||||||
description: "Trigger an automation when a HTTP POST webhook is hit",
|
description: "Trigger an automation when a HTTP POST webhook is hit",
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
Row,
|
Row,
|
||||||
AutomationData,
|
AutomationData,
|
||||||
AutomationJob,
|
AutomationJob,
|
||||||
|
AutomationEventType,
|
||||||
UpdatedRowEventEmitter,
|
UpdatedRowEventEmitter,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { executeInThread } from "../threads/automation"
|
import { executeInThread } from "../threads/automation"
|
||||||
|
@ -71,28 +72,31 @@ async function queueRelevantRowAutomations(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter.on("row:save", async function (event: UpdatedRowEventEmitter) {
|
emitter.on(
|
||||||
|
AutomationEventType.ROW_SAVE,
|
||||||
|
async function (event: UpdatedRowEventEmitter) {
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
if (!event || !event.row || !event.row.tableId) {
|
if (!event || !event.row || !event.row.tableId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await queueRelevantRowAutomations(event, "row:save")
|
await queueRelevantRowAutomations(event, AutomationEventType.ROW_SAVE)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
emitter.on(AutomationEventType.ROW_UPDATE, async function (event) {
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (!event || !event.row || !event.row.tableId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await queueRelevantRowAutomations(event, AutomationEventType.ROW_UPDATE)
|
||||||
})
|
})
|
||||||
|
|
||||||
emitter.on("row:update", async function (event) {
|
emitter.on(AutomationEventType.ROW_DELETE, async function (event) {
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
if (!event || !event.row || !event.row.tableId) {
|
if (!event || !event.row || !event.row.tableId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await queueRelevantRowAutomations(event, "row:update")
|
await queueRelevantRowAutomations(event, AutomationEventType.ROW_DELETE)
|
||||||
})
|
|
||||||
|
|
||||||
emitter.on("row:delete", async function (event) {
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (!event || !event.row || !event.row.tableId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await queueRelevantRowAutomations(event, "row:delete")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function externalTrigger(
|
export async function externalTrigger(
|
||||||
|
@ -118,7 +122,6 @@ export async function externalTrigger(
|
||||||
}
|
}
|
||||||
params.fields = coercedFields
|
params.fields = coercedFields
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: AutomationData = { automation, event: params as any }
|
const data: AutomationData = { automation, event: params as any }
|
||||||
if (getResponses) {
|
if (getResponses) {
|
||||||
data.event = {
|
data.event = {
|
||||||
|
|
|
@ -75,16 +75,12 @@ const environment = {
|
||||||
AUTOMATION_MAX_ITERATIONS:
|
AUTOMATION_MAX_ITERATIONS:
|
||||||
parseIntSafe(process.env.AUTOMATION_MAX_ITERATIONS) ||
|
parseIntSafe(process.env.AUTOMATION_MAX_ITERATIONS) ||
|
||||||
DEFAULTS.AUTOMATION_MAX_ITERATIONS,
|
DEFAULTS.AUTOMATION_MAX_ITERATIONS,
|
||||||
SENDGRID_API_KEY: process.env.SENDGRID_API_KEY,
|
|
||||||
DYNAMO_ENDPOINT: process.env.DYNAMO_ENDPOINT,
|
DYNAMO_ENDPOINT: process.env.DYNAMO_ENDPOINT,
|
||||||
QUERY_THREAD_TIMEOUT: QUERY_THREAD_TIMEOUT,
|
QUERY_THREAD_TIMEOUT: QUERY_THREAD_TIMEOUT,
|
||||||
AUTOMATION_THREAD_TIMEOUT:
|
AUTOMATION_THREAD_TIMEOUT:
|
||||||
parseIntSafe(process.env.AUTOMATION_THREAD_TIMEOUT) ||
|
parseIntSafe(process.env.AUTOMATION_THREAD_TIMEOUT) ||
|
||||||
DEFAULT_AUTOMATION_TIMEOUT,
|
DEFAULT_AUTOMATION_TIMEOUT,
|
||||||
BB_ADMIN_USER_EMAIL: process.env.BB_ADMIN_USER_EMAIL,
|
|
||||||
BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD,
|
|
||||||
PLUGINS_DIR: process.env.PLUGINS_DIR || DEFAULTS.PLUGINS_DIR,
|
PLUGINS_DIR: process.env.PLUGINS_DIR || DEFAULTS.PLUGINS_DIR,
|
||||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
|
||||||
MAX_IMPORT_SIZE_MB: process.env.MAX_IMPORT_SIZE_MB,
|
MAX_IMPORT_SIZE_MB: process.env.MAX_IMPORT_SIZE_MB,
|
||||||
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
|
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
|
||||||
// SQL
|
// SQL
|
||||||
|
@ -92,6 +88,7 @@ const environment = {
|
||||||
SQL_LOGGING_ENABLE: process.env.SQL_LOGGING_ENABLE,
|
SQL_LOGGING_ENABLE: process.env.SQL_LOGGING_ENABLE,
|
||||||
SQL_ALIASING_DISABLE: process.env.SQL_ALIASING_DISABLE,
|
SQL_ALIASING_DISABLE: process.env.SQL_ALIASING_DISABLE,
|
||||||
SQS_SEARCH_ENABLE: process.env.SQS_SEARCH_ENABLE,
|
SQS_SEARCH_ENABLE: process.env.SQS_SEARCH_ENABLE,
|
||||||
|
SQS_MIGRATION_ENABLE: process.env.SQS_MIGRATION_ENABLE,
|
||||||
// flags
|
// flags
|
||||||
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
|
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
|
||||||
DISABLE_THREADING: process.env.DISABLE_THREADING,
|
DISABLE_THREADING: process.env.DISABLE_THREADING,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
tenancy,
|
tenancy,
|
||||||
users,
|
users,
|
||||||
cache,
|
cache,
|
||||||
|
env as coreEnv,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { watch } from "../watch"
|
import { watch } from "../watch"
|
||||||
import * as automations from "../automations"
|
import * as automations from "../automations"
|
||||||
|
@ -132,8 +133,8 @@ export async function startup(
|
||||||
// check and create admin user if required
|
// check and create admin user if required
|
||||||
// this must be run after the api has been initialised due to
|
// this must be run after the api has been initialised due to
|
||||||
// the app user sync
|
// the app user sync
|
||||||
const bbAdminEmail = env.BB_ADMIN_USER_EMAIL,
|
const bbAdminEmail = coreEnv.BB_ADMIN_USER_EMAIL,
|
||||||
bbAdminPassword = env.BB_ADMIN_USER_PASSWORD
|
bbAdminPassword = coreEnv.BB_ADMIN_USER_PASSWORD
|
||||||
if (
|
if (
|
||||||
env.SELF_HOSTED &&
|
env.SELF_HOSTED &&
|
||||||
!env.MULTI_TENANCY &&
|
!env.MULTI_TENANCY &&
|
||||||
|
|
|
@ -14,11 +14,15 @@ describe("check BB_ADMIN environment variables", () => {
|
||||||
await tenancy.doInTenant(tenancy.DEFAULT_TENANT_ID, async () => {
|
await tenancy.doInTenant(tenancy.DEFAULT_TENANT_ID, async () => {
|
||||||
await config.withEnv(
|
await config.withEnv(
|
||||||
{
|
{
|
||||||
BB_ADMIN_USER_EMAIL: EMAIL,
|
|
||||||
BB_ADMIN_USER_PASSWORD: PASSWORD,
|
|
||||||
MULTI_TENANCY: "0",
|
MULTI_TENANCY: "0",
|
||||||
SELF_HOSTED: "1",
|
SELF_HOSTED: "1",
|
||||||
},
|
},
|
||||||
|
() =>
|
||||||
|
config.withCoreEnv(
|
||||||
|
{
|
||||||
|
BB_ADMIN_USER_EMAIL: EMAIL,
|
||||||
|
BB_ADMIN_USER_PASSWORD: PASSWORD,
|
||||||
|
},
|
||||||
async () => {
|
async () => {
|
||||||
await startup({ rerun: true })
|
await startup({ rerun: true })
|
||||||
const user = await users.getGlobalUserByEmail(EMAIL, {
|
const user = await users.getGlobalUserByEmail(EMAIL, {
|
||||||
|
@ -26,9 +30,12 @@ describe("check BB_ADMIN environment variables", () => {
|
||||||
})
|
})
|
||||||
expect(user).toBeDefined()
|
expect(user).toBeDefined()
|
||||||
expect(user?.password).toBeDefined()
|
expect(user?.password).toBeDefined()
|
||||||
expect(await utils.compare(PASSWORD, user?.password!)).toEqual(true)
|
expect(await utils.compare(PASSWORD, user?.password!)).toEqual(
|
||||||
|
true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -290,7 +290,7 @@ export default class TestConfiguration {
|
||||||
* that can be called to reset the environment variables to their original values.
|
* that can be called to reset the environment variables to their original values.
|
||||||
*/
|
*/
|
||||||
setCoreEnv(newEnvVars: Partial<typeof coreEnv>): () => void {
|
setCoreEnv(newEnvVars: Partial<typeof coreEnv>): () => void {
|
||||||
const oldEnv = cloneDeep(env)
|
const oldEnv = cloneDeep(coreEnv)
|
||||||
|
|
||||||
let key: keyof typeof newEnvVars
|
let key: keyof typeof newEnvVars
|
||||||
for (key in newEnvVars) {
|
for (key in newEnvVars) {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
Query,
|
Query,
|
||||||
Webhook,
|
Webhook,
|
||||||
WebhookActionType,
|
WebhookActionType,
|
||||||
|
AutomationEventType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { LoopInput, LoopStepType } from "../../definitions/automations"
|
import { LoopInput, LoopStepType } from "../../definitions/automations"
|
||||||
import { merge } from "lodash"
|
import { merge } from "lodash"
|
||||||
|
@ -305,7 +306,7 @@ export function loopAutomation(
|
||||||
trigger: {
|
trigger: {
|
||||||
id: "a",
|
id: "a",
|
||||||
type: "TRIGGER",
|
type: "TRIGGER",
|
||||||
event: "row:save",
|
event: AutomationEventType.ROW_SAVE,
|
||||||
stepId: AutomationTriggerStepId.ROW_SAVED,
|
stepId: AutomationTriggerStepId.ROW_SAVED,
|
||||||
inputs: {
|
inputs: {
|
||||||
tableId,
|
tableId,
|
||||||
|
@ -347,7 +348,7 @@ export function collectAutomation(tableId?: string): Automation {
|
||||||
trigger: {
|
trigger: {
|
||||||
id: "a",
|
id: "a",
|
||||||
type: "TRIGGER",
|
type: "TRIGGER",
|
||||||
event: "row:save",
|
event: AutomationEventType.ROW_SAVE,
|
||||||
stepId: AutomationTriggerStepId.ROW_SAVED,
|
stepId: AutomationTriggerStepId.ROW_SAVED,
|
||||||
inputs: {
|
inputs: {
|
||||||
tableId,
|
tableId,
|
||||||
|
|
|
@ -50,6 +50,13 @@ export const TYPE_TRANSFORM_MAP: any = {
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
parse: parseArrayString,
|
parse: parseArrayString,
|
||||||
},
|
},
|
||||||
|
[FieldType.BB_REFERENCE]: {
|
||||||
|
//@ts-ignore
|
||||||
|
[null]: [],
|
||||||
|
//@ts-ignore
|
||||||
|
[undefined]: undefined,
|
||||||
|
parse: parseArrayString,
|
||||||
|
},
|
||||||
[FieldType.STRING]: {
|
[FieldType.STRING]: {
|
||||||
"": null,
|
"": null,
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
|
@ -113,6 +120,9 @@ export const TYPE_TRANSFORM_MAP: any = {
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
parse: parseArrayString,
|
parse: parseArrayString,
|
||||||
},
|
},
|
||||||
|
[FieldType.ATTACHMENT_SINGLE]: {
|
||||||
|
"": null,
|
||||||
|
},
|
||||||
[FieldType.BOOLEAN]: {
|
[FieldType.BOOLEAN]: {
|
||||||
"": null,
|
"": null,
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
|
|
|
@ -209,10 +209,22 @@ describe("rowProcessor - inputProcessing", () => {
|
||||||
|
|
||||||
const { row } = await inputProcessing(userId, table, newRow)
|
const { row } = await inputProcessing(userId, table, newRow)
|
||||||
|
|
||||||
|
if (userValue === undefined) {
|
||||||
|
// The 'user' field is omitted
|
||||||
|
expect(row).toEqual({
|
||||||
|
name: "Jack",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// The update is processed if null or "". 'user' is changed to an empty array.
|
||||||
|
expect(row).toEqual({
|
||||||
|
name: "Jack",
|
||||||
|
user: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
bbReferenceProcessor.processInputBBReferences
|
bbReferenceProcessor.processInputBBReferences
|
||||||
).not.toHaveBeenCalled()
|
).not.toHaveBeenCalled()
|
||||||
expect(row).toEqual(newRow)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -255,6 +255,15 @@ export type BucketedContent = AutomationAttachmentContent & {
|
||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AutomationEventType {
|
||||||
|
ROW_SAVE = "row:save",
|
||||||
|
ROW_UPDATE = "row:update",
|
||||||
|
ROW_DELETE = "row:delete",
|
||||||
|
APP_TRIGGER = "app:trigger",
|
||||||
|
CRON_TRIGGER = "cron:trigger",
|
||||||
|
WEBHOOK_TRIGGER = "web:trigger",
|
||||||
|
}
|
||||||
|
|
||||||
export type UpdatedRowEventEmitter = {
|
export type UpdatedRowEventEmitter = {
|
||||||
row: Row
|
row: Row
|
||||||
oldRow: Row
|
oldRow: Row
|
||||||
|
|
|
@ -3,4 +3,5 @@ export interface SaveUserOpts {
|
||||||
requirePassword?: boolean
|
requirePassword?: boolean
|
||||||
currentUserId?: string
|
currentUserId?: string
|
||||||
skipPasswordValidation?: boolean
|
skipPasswordValidation?: boolean
|
||||||
|
allowChangingEmail?: boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -574,6 +574,41 @@ describe("scim", () => {
|
||||||
|
|
||||||
expect(events.user.updated).toHaveBeenCalledTimes(1)
|
expect(events.user.updated).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("an existing user's email can be updated", async () => {
|
||||||
|
const newEmail = structures.generator.email()
|
||||||
|
const body: ScimUpdateRequest = {
|
||||||
|
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||||
|
Operations: [
|
||||||
|
{
|
||||||
|
op: "Replace",
|
||||||
|
path: 'emails[type eq "work"].value',
|
||||||
|
value: newEmail,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await patchScimUser({ id: user.id, body })
|
||||||
|
|
||||||
|
const expectedScimUser: ScimUserResponse = {
|
||||||
|
...user,
|
||||||
|
emails: [
|
||||||
|
{
|
||||||
|
value: newEmail,
|
||||||
|
type: "work",
|
||||||
|
primary: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(response).toEqual(expectedScimUser)
|
||||||
|
|
||||||
|
const persistedUser = await config.api.scimUsersAPI.find(user.id)
|
||||||
|
expect(persistedUser).toEqual(expectedScimUser)
|
||||||
|
|
||||||
|
expect((await config.api.users.getUser(user.id)).body).toEqual(
|
||||||
|
expect.objectContaining({ _id: user.id, email: newEmail })
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("DELETE /api/global/scim/v2/users/:id", () => {
|
describe("DELETE /api/global/scim/v2/users/:id", () => {
|
||||||
|
|
|
@ -16112,10 +16112,10 @@ mongodb-connection-string-url@^3.0.0:
|
||||||
"@types/whatwg-url" "^11.0.2"
|
"@types/whatwg-url" "^11.0.2"
|
||||||
whatwg-url "^13.0.0"
|
whatwg-url "^13.0.0"
|
||||||
|
|
||||||
mongodb@^6.8.0:
|
mongodb@6.7.0:
|
||||||
version "6.8.0"
|
version "6.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-6.8.0.tgz#680450f113cdea6d2d9f7121fe57cd29111fd2ce"
|
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-6.7.0.tgz#f86e51e6530e6a2ca4a99d7cfdf6f409223ac199"
|
||||||
integrity sha512-HGQ9NWDle5WvwMnrvUxsFYPd3JEbqD3RgABHBQRuoCEND0qzhsd0iH5ypHsf1eJ+sXmvmyKpP+FLOKY8Il7jMw==
|
integrity sha512-TMKyHdtMcO0fYBNORiYdmM25ijsHs+Njs963r4Tro4OQZzqYigAzYQouwWRg4OIaiLRUEGUh/1UAcH5lxdSLIA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@mongodb-js/saslprep" "^1.1.5"
|
"@mongodb-js/saslprep" "^1.1.5"
|
||||||
bson "^6.7.0"
|
bson "^6.7.0"
|
||||||
|
|
Loading…
Reference in New Issue