Merge branch 'master' into sqs-2.0-image
This commit is contained in:
commit
1db269f570
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "2.30.2",
|
"version": "2.30.3",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -255,4 +255,5 @@ export const flags = new FlagSet({
|
||||||
GOOGLE_SHEETS: Flag.boolean(false),
|
GOOGLE_SHEETS: Flag.boolean(false),
|
||||||
USER_GROUPS: Flag.boolean(false),
|
USER_GROUPS: Flag.boolean(false),
|
||||||
ONBOARDING_TOUR: Flag.boolean(false),
|
ONBOARDING_TOUR: Flag.boolean(false),
|
||||||
|
DEFAULT_VALUES: Flag.boolean(true),
|
||||||
})
|
})
|
||||||
|
|
|
@ -70,6 +70,7 @@
|
||||||
|
|
||||||
// Stop unnecessary rendering
|
// Stop unnecessary rendering
|
||||||
const memoBlock = memo(block)
|
const memoBlock = memo(block)
|
||||||
|
const memoEnvVariables = memo($environment.variables)
|
||||||
|
|
||||||
const rowTriggers = [
|
const rowTriggers = [
|
||||||
TriggerStepID.ROW_UPDATED,
|
TriggerStepID.ROW_UPDATED,
|
||||||
|
@ -91,11 +92,20 @@
|
||||||
let insertAtPos, getCaretPosition
|
let insertAtPos, getCaretPosition
|
||||||
let stepLayouts = {}
|
let stepLayouts = {}
|
||||||
|
|
||||||
|
$: memoEnvVariables.set($environment.variables)
|
||||||
$: memoBlock.set(block)
|
$: memoBlock.set(block)
|
||||||
|
|
||||||
$: filters = lookForFilters(schemaProperties) || []
|
$: filters = lookForFilters(schemaProperties) || []
|
||||||
$: tempFilters = filters
|
$: tempFilters = filters
|
||||||
$: stepId = block.stepId
|
$: stepId = $memoBlock.stepId
|
||||||
$: bindings = getAvailableBindings(block, $selectedAutomation?.definition)
|
|
||||||
|
$: automationBindings = getAvailableBindings(
|
||||||
|
$memoBlock,
|
||||||
|
$selectedAutomation?.definition
|
||||||
|
)
|
||||||
|
$: environmentBindings = buildEnvironmentBindings($memoEnvVariables)
|
||||||
|
$: bindings = [...automationBindings, ...environmentBindings]
|
||||||
|
|
||||||
$: getInputData(testData, $memoBlock.inputs)
|
$: getInputData(testData, $memoBlock.inputs)
|
||||||
$: tableId = inputData ? inputData.tableId : null
|
$: tableId = inputData ? inputData.tableId : null
|
||||||
$: table = tableId
|
$: table = tableId
|
||||||
|
@ -110,7 +120,7 @@
|
||||||
{ allowLinks: true }
|
{ allowLinks: true }
|
||||||
)
|
)
|
||||||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||||
$: isTrigger = block?.type === AutomationStepType.TRIGGER
|
$: isTrigger = $memoBlock?.type === AutomationStepType.TRIGGER
|
||||||
$: codeMode =
|
$: codeMode =
|
||||||
stepId === AutomationActionStepId.EXECUTE_BASH
|
stepId === AutomationActionStepId.EXECUTE_BASH
|
||||||
? EditorModes.Handlebars
|
? EditorModes.Handlebars
|
||||||
|
@ -119,13 +129,30 @@
|
||||||
disableWrapping: true,
|
disableWrapping: true,
|
||||||
})
|
})
|
||||||
$: editingJs = codeMode === EditorModes.JS
|
$: editingJs = codeMode === EditorModes.JS
|
||||||
$: requiredProperties = isTestModal ? [] : block.schema["inputs"].required
|
$: requiredProperties = isTestModal
|
||||||
|
? []
|
||||||
|
: $memoBlock.schema["inputs"].required
|
||||||
|
|
||||||
$: stepCompletions =
|
$: stepCompletions =
|
||||||
codeMode === EditorModes.Handlebars
|
codeMode === EditorModes.Handlebars
|
||||||
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||||
: []
|
: []
|
||||||
|
|
||||||
|
const buildEnvironmentBindings = () => {
|
||||||
|
if ($licensing.environmentVariablesEnabled) {
|
||||||
|
return getEnvironmentBindings().map(binding => {
|
||||||
|
return {
|
||||||
|
...binding,
|
||||||
|
display: {
|
||||||
|
...binding.display,
|
||||||
|
rank: 98,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
@ -151,9 +178,9 @@
|
||||||
|
|
||||||
// Store for any UX related data
|
// Store for any UX related data
|
||||||
const stepStore = writable({})
|
const stepStore = writable({})
|
||||||
$: currentStep = $stepStore?.[block.id]
|
$: stepState = $stepStore?.[block.id]
|
||||||
|
|
||||||
$: customStepLayouts($memoBlock, schemaProperties, currentStep)
|
$: customStepLayouts($memoBlock, schemaProperties, stepState)
|
||||||
|
|
||||||
const customStepLayouts = block => {
|
const customStepLayouts = block => {
|
||||||
if (
|
if (
|
||||||
|
@ -185,7 +212,6 @@
|
||||||
onChange: e => {
|
onChange: e => {
|
||||||
onChange({ ["revision"]: e.detail })
|
onChange({ ["revision"]: e.detail })
|
||||||
},
|
},
|
||||||
bindings,
|
|
||||||
updateOnChange: false,
|
updateOnChange: false,
|
||||||
forceModal: true,
|
forceModal: true,
|
||||||
},
|
},
|
||||||
|
@ -214,7 +240,6 @@
|
||||||
onChange: e => {
|
onChange: e => {
|
||||||
onChange({ [rowIdentifier]: e.detail })
|
onChange({ [rowIdentifier]: e.detail })
|
||||||
},
|
},
|
||||||
bindings,
|
|
||||||
updateOnChange: false,
|
updateOnChange: false,
|
||||||
forceModal: true,
|
forceModal: true,
|
||||||
},
|
},
|
||||||
|
@ -275,7 +300,7 @@
|
||||||
isUpdateRow: block.stepId === ActionStepID.UPDATE_ROW,
|
isUpdateRow: block.stepId === ActionStepID.UPDATE_ROW,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTestModal && currentStep?.rowType === "oldRow") {
|
if (isTestModal && stepState?.rowType === "oldRow") {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: RowSelector,
|
type: RowSelector,
|
||||||
|
@ -722,22 +747,9 @@
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Environment bindings
|
|
||||||
if ($licensing.environmentVariablesEnabled) {
|
|
||||||
bindings = bindings.concat(
|
|
||||||
getEnvironmentBindings().map(binding => {
|
|
||||||
return {
|
|
||||||
...binding,
|
|
||||||
display: {
|
|
||||||
...binding.display,
|
|
||||||
rank: 98,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
function lookForFilters(properties) {
|
function lookForFilters(properties) {
|
||||||
if (!properties) {
|
if (!properties) {
|
||||||
return []
|
return []
|
||||||
|
@ -770,7 +782,7 @@
|
||||||
drawer.hide()
|
drawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function canShowField(key, value) {
|
function canShowField(value) {
|
||||||
const dependsOn = value?.dependsOn
|
const dependsOn = value?.dependsOn
|
||||||
return !dependsOn || !!inputData[dependsOn]
|
return !dependsOn || !!inputData[dependsOn]
|
||||||
}
|
}
|
||||||
|
@ -829,6 +841,7 @@
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={config.type}
|
this={config.type}
|
||||||
{...config.props}
|
{...config.props}
|
||||||
|
{bindings}
|
||||||
on:change={config.props.onChange}
|
on:change={config.props.onChange}
|
||||||
/>
|
/>
|
||||||
</PropField>
|
</PropField>
|
||||||
|
@ -836,6 +849,7 @@
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={config.type}
|
this={config.type}
|
||||||
{...config.props}
|
{...config.props}
|
||||||
|
{bindings}
|
||||||
on:change={config.props.onChange}
|
on:change={config.props.onChange}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -13,6 +13,10 @@
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
|
import {
|
||||||
|
readableToRuntimeBinding,
|
||||||
|
runtimeToReadableBinding,
|
||||||
|
} from "dataBinding"
|
||||||
|
|
||||||
export let onChange
|
export let onChange
|
||||||
export let field
|
export let field
|
||||||
|
@ -30,6 +34,8 @@
|
||||||
return clone
|
return clone
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$: readableValue = runtimeToReadableBinding(parsedBindings, fieldData)
|
||||||
|
|
||||||
let attachmentTypes = [
|
let attachmentTypes = [
|
||||||
FieldType.ATTACHMENTS,
|
FieldType.ATTACHMENTS,
|
||||||
FieldType.ATTACHMENT_SINGLE,
|
FieldType.ATTACHMENT_SINGLE,
|
||||||
|
@ -132,11 +138,11 @@
|
||||||
/>
|
/>
|
||||||
{:else if schema.type === "longform"}
|
{:else if schema.type === "longform"}
|
||||||
<TextArea
|
<TextArea
|
||||||
value={fieldData}
|
value={readableValue}
|
||||||
on:change={e =>
|
on:change={e =>
|
||||||
onChange({
|
onChange({
|
||||||
row: {
|
row: {
|
||||||
[field]: e.detail,
|
[field]: readableToRuntimeBinding(parsedBindings, e.detail),
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
@ -144,11 +150,11 @@
|
||||||
<span>
|
<span>
|
||||||
<div class="field-wrap json-field">
|
<div class="field-wrap json-field">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={fieldData}
|
value={readableValue}
|
||||||
on:change={e => {
|
on:blur={e => {
|
||||||
onChange({
|
onChange({
|
||||||
row: {
|
row: {
|
||||||
[field]: e.detail,
|
[field]: readableToRuntimeBinding(parsedBindings, e.detail),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Label } from "@budibase/bbui"
|
import { Label } from "@budibase/bbui"
|
||||||
import { onMount, createEventDispatcher } from "svelte"
|
import { onMount, createEventDispatcher, onDestroy } from "svelte"
|
||||||
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
|
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -58,6 +58,64 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let textarea
|
||||||
|
let editor
|
||||||
|
let mounted = false
|
||||||
|
let isEditorInitialised = false
|
||||||
|
let queuedRefresh = false
|
||||||
|
|
||||||
|
// Theming!
|
||||||
|
let currentTheme = $themeStore?.theme
|
||||||
|
let isDark = !currentTheme.includes("light")
|
||||||
|
let themeConfig = new Compartment()
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (autofocus && isEditorInitialised) {
|
||||||
|
editor.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init when all elements are ready
|
||||||
|
$: if (mounted && !isEditorInitialised) {
|
||||||
|
isEditorInitialised = true
|
||||||
|
initEditor()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme change
|
||||||
|
$: if (mounted && isEditorInitialised && $themeStore?.theme) {
|
||||||
|
if (currentTheme != $themeStore?.theme) {
|
||||||
|
currentTheme = $themeStore?.theme
|
||||||
|
isDark = !currentTheme.includes("light")
|
||||||
|
|
||||||
|
// Issue theme compartment update
|
||||||
|
editor.dispatch({
|
||||||
|
effects: themeConfig.reconfigure([...(isDark ? [oneDark] : [])]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait to try and gracefully replace
|
||||||
|
$: refresh(value, isEditorInitialised, mounted)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will refresh the editor contents only after
|
||||||
|
* it has been fully initialised
|
||||||
|
* @param value {string} the editor value
|
||||||
|
*/
|
||||||
|
const refresh = (value, initialised, mounted) => {
|
||||||
|
if (!initialised || !mounted) {
|
||||||
|
queuedRefresh = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editor.state.doc.toString() !== value || queuedRefresh) {
|
||||||
|
editor.dispatch({
|
||||||
|
changes: { from: 0, to: editor.state.doc.length, insert: value },
|
||||||
|
})
|
||||||
|
queuedRefresh = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Export a function to expose caret position
|
// Export a function to expose caret position
|
||||||
export const getCaretPosition = () => {
|
export const getCaretPosition = () => {
|
||||||
const selection_range = editor.state.selection.ranges[0]
|
const selection_range = editor.state.selection.ranges[0]
|
||||||
|
@ -132,11 +190,6 @@
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Theming!
|
|
||||||
let currentTheme = $themeStore?.theme
|
|
||||||
let isDark = !currentTheme.includes("light")
|
|
||||||
let themeConfig = new Compartment()
|
|
||||||
|
|
||||||
const indentWithTabCustom = {
|
const indentWithTabCustom = {
|
||||||
key: "Tab",
|
key: "Tab",
|
||||||
run: view => {
|
run: view => {
|
||||||
|
@ -253,6 +306,11 @@
|
||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
foldGutter(),
|
foldGutter(),
|
||||||
keymap.of(buildKeymap()),
|
keymap.of(buildKeymap()),
|
||||||
|
EditorView.domEventHandlers({
|
||||||
|
blur: () => {
|
||||||
|
dispatch("blur", editor.state.doc.toString())
|
||||||
|
},
|
||||||
|
}),
|
||||||
EditorView.updateListener.of(v => {
|
EditorView.updateListener.of(v => {
|
||||||
const docStr = v.state.doc?.toString()
|
const docStr = v.state.doc?.toString()
|
||||||
if (docStr === value) {
|
if (docStr === value) {
|
||||||
|
@ -266,11 +324,6 @@
|
||||||
return complete
|
return complete
|
||||||
}
|
}
|
||||||
|
|
||||||
let textarea
|
|
||||||
let editor
|
|
||||||
let mounted = false
|
|
||||||
let isEditorInitialised = false
|
|
||||||
|
|
||||||
const initEditor = () => {
|
const initEditor = () => {
|
||||||
const baseExtensions = buildBaseExtensions()
|
const baseExtensions = buildBaseExtensions()
|
||||||
|
|
||||||
|
@ -281,37 +334,13 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
|
||||||
if (autofocus && isEditorInitialised) {
|
|
||||||
editor.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init when all elements are ready
|
|
||||||
$: if (mounted && !isEditorInitialised) {
|
|
||||||
isEditorInitialised = true
|
|
||||||
initEditor()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Theme change
|
|
||||||
$: if (mounted && isEditorInitialised && $themeStore?.theme) {
|
|
||||||
if (currentTheme != $themeStore?.theme) {
|
|
||||||
currentTheme = $themeStore?.theme
|
|
||||||
isDark = !currentTheme.includes("light")
|
|
||||||
|
|
||||||
// Issue theme compartment update
|
|
||||||
editor.dispatch({
|
|
||||||
effects: themeConfig.reconfigure([...(isDark ? [oneDark] : [])]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
mounted = true
|
mounted = true
|
||||||
return () => {
|
})
|
||||||
if (editor) {
|
|
||||||
editor.destroy()
|
onDestroy(() => {
|
||||||
}
|
if (editor) {
|
||||||
|
editor.destroy()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
export let allowJS = true
|
export let allowJS = true
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let drawerLeft
|
|
||||||
export let type
|
export let type
|
||||||
export let schema
|
export let schema
|
||||||
|
|
||||||
|
@ -170,14 +169,7 @@
|
||||||
<Icon disabled={isJS} size="S" name="Close" />
|
<Icon disabled={isJS} size="S" name="Close" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<slot
|
<slot />
|
||||||
{label}
|
|
||||||
{disabled}
|
|
||||||
readonly={isJS}
|
|
||||||
value={isJS ? "(JavaScript function)" : readableValue}
|
|
||||||
{placeholder}
|
|
||||||
{updateOnChange}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if !disabled && type !== "formula" && !disabled && !attachmentTypes.includes(type)}
|
{#if !disabled && type !== "formula" && !disabled && !attachmentTypes.includes(type)}
|
||||||
<div
|
<div
|
||||||
|
@ -195,7 +187,7 @@
|
||||||
on:drawerShow
|
on:drawerShow
|
||||||
bind:this={bindingDrawer}
|
bind:this={bindingDrawer}
|
||||||
title={title ?? placeholder ?? "Bindings"}
|
title={title ?? placeholder ?? "Bindings"}
|
||||||
left={drawerLeft}
|
forceModal={true}
|
||||||
>
|
>
|
||||||
<Button cta slot="buttons" on:click={saveBinding}>Save</Button>
|
<Button cta slot="buttons" on:click={saveBinding}>Save</Button>
|
||||||
<svelte:component
|
<svelte:component
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getComponentContexts } from "dataBinding"
|
import { getAllComponentContexts } from "dataBinding"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
|
|
||||||
// Generates bindings for all components that provider "datasource like"
|
// Generates bindings for all components that provider "datasource like"
|
||||||
|
@ -7,7 +7,7 @@ import { capitalise } from "helpers"
|
||||||
// Some examples are saving rows or duplicating rows.
|
// Some examples are saving rows or duplicating rows.
|
||||||
export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
|
export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
|
||||||
// Get all form context providers
|
// Get all form context providers
|
||||||
const formComponentContexts = getComponentContexts(
|
const formComponentContexts = getAllComponentContexts(
|
||||||
asset,
|
asset,
|
||||||
componentId,
|
componentId,
|
||||||
"form",
|
"form",
|
||||||
|
@ -16,7 +16,7 @@ export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// Get all schema context providers
|
// Get all schema context providers
|
||||||
const schemaComponentContexts = getComponentContexts(
|
const schemaComponentContexts = getAllComponentContexts(
|
||||||
asset,
|
asset,
|
||||||
componentId,
|
componentId,
|
||||||
"schema",
|
"schema",
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
findAllMatchingComponents,
|
findAllMatchingComponents,
|
||||||
findComponent,
|
findComponent,
|
||||||
findComponentPath,
|
findComponentPath,
|
||||||
|
getComponentContexts,
|
||||||
} from "helpers/components"
|
} from "helpers/components"
|
||||||
import {
|
import {
|
||||||
componentStore,
|
componentStore,
|
||||||
|
@ -213,7 +214,7 @@ export const getComponentBindableProperties = (asset, componentId) => {
|
||||||
* both global and local bindings, taking into account a component's position
|
* both global and local bindings, taking into account a component's position
|
||||||
* in the component tree.
|
* in the component tree.
|
||||||
*/
|
*/
|
||||||
export const getComponentContexts = (
|
export const getAllComponentContexts = (
|
||||||
asset,
|
asset,
|
||||||
componentId,
|
componentId,
|
||||||
type,
|
type,
|
||||||
|
@ -229,11 +230,6 @@ export const getComponentContexts = (
|
||||||
|
|
||||||
// Processes all contexts exposed by a component
|
// Processes all contexts exposed by a component
|
||||||
const processContexts = scope => component => {
|
const processContexts = scope => component => {
|
||||||
const def = componentStore.getDefinition(component._component)
|
|
||||||
if (!def?.context) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out global contexts not in the same branch.
|
// Filter out global contexts not in the same branch.
|
||||||
// Global contexts are only valid if their branch root is an ancestor of
|
// Global contexts are only valid if their branch root is an ancestor of
|
||||||
// this component.
|
// this component.
|
||||||
|
@ -242,8 +238,8 @@ export const getComponentContexts = (
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process all contexts provided by this component
|
const componentType = component._component
|
||||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
const contexts = getComponentContexts(componentType)
|
||||||
contexts.forEach(context => {
|
contexts.forEach(context => {
|
||||||
// Ensure type matches
|
// Ensure type matches
|
||||||
if (type && context.type !== type) {
|
if (type && context.type !== type) {
|
||||||
|
@ -261,7 +257,7 @@ export const getComponentContexts = (
|
||||||
if (!map[component._id]) {
|
if (!map[component._id]) {
|
||||||
map[component._id] = {
|
map[component._id] = {
|
||||||
component,
|
component,
|
||||||
definition: def,
|
definition: componentStore.getDefinition(componentType),
|
||||||
contexts: [],
|
contexts: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -286,7 +282,7 @@ export const getComponentContexts = (
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all data provider components above a component.
|
* Gets all components available to this component that expose a certain action
|
||||||
*/
|
*/
|
||||||
export const getActionProviders = (
|
export const getActionProviders = (
|
||||||
asset,
|
asset,
|
||||||
|
@ -294,36 +290,30 @@ export const getActionProviders = (
|
||||||
actionType,
|
actionType,
|
||||||
options = { includeSelf: false }
|
options = { includeSelf: false }
|
||||||
) => {
|
) => {
|
||||||
if (!asset) {
|
const contexts = getAllComponentContexts(asset, componentId, "action", {
|
||||||
return []
|
includeSelf: options?.includeSelf,
|
||||||
}
|
|
||||||
|
|
||||||
// Get all components
|
|
||||||
const components = findAllComponents(asset.props)
|
|
||||||
|
|
||||||
// Find matching contexts and generate bindings
|
|
||||||
let providers = []
|
|
||||||
components.forEach(component => {
|
|
||||||
if (!options?.includeSelf && component._id === componentId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const def = componentStore.getDefinition(component._component)
|
|
||||||
const actions = (def?.actions || []).map(action => {
|
|
||||||
return typeof action === "string" ? { type: action } : action
|
|
||||||
})
|
|
||||||
const action = actions.find(x => x.type === actionType)
|
|
||||||
if (action) {
|
|
||||||
let runtimeBinding = component._id
|
|
||||||
if (action.suffix) {
|
|
||||||
runtimeBinding += `-${action.suffix}`
|
|
||||||
}
|
|
||||||
providers.push({
|
|
||||||
readableBinding: component._instanceName,
|
|
||||||
runtimeBinding,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return providers
|
return (
|
||||||
|
contexts
|
||||||
|
// Find the definition of the action in question, if one is provided
|
||||||
|
.map(context => ({
|
||||||
|
...context,
|
||||||
|
action: context.contexts[0]?.actions?.find(x => x.type === actionType),
|
||||||
|
}))
|
||||||
|
// Filter out contexts which don't have this action
|
||||||
|
.filter(({ action }) => action != null)
|
||||||
|
// Generate bindings for this component and action
|
||||||
|
.map(({ component, action }) => {
|
||||||
|
let runtimeBinding = component._id
|
||||||
|
if (action.suffix) {
|
||||||
|
runtimeBinding += `-${action.suffix}`
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
readableBinding: component._instanceName,
|
||||||
|
runtimeBinding,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -371,7 +361,7 @@ export const getDatasourceForProvider = (asset, component) => {
|
||||||
*/
|
*/
|
||||||
const getContextBindings = (asset, componentId) => {
|
const getContextBindings = (asset, componentId) => {
|
||||||
// Get all available contexts for this component
|
// Get all available contexts for this component
|
||||||
const componentContexts = getComponentContexts(asset, componentId)
|
const componentContexts = getAllComponentContexts(asset, componentId)
|
||||||
|
|
||||||
// Generate bindings for each context
|
// Generate bindings for each context
|
||||||
return componentContexts
|
return componentContexts
|
||||||
|
|
|
@ -228,6 +228,25 @@ export const getComponentName = component => {
|
||||||
return componentDefinition.friendlyName || componentDefinition.name || ""
|
return componentDefinition.friendlyName || componentDefinition.name || ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gets all contexts exposed by a certain component type, including actions
|
||||||
|
export const getComponentContexts = component => {
|
||||||
|
const def = componentStore.getDefinition(component)
|
||||||
|
let contexts = []
|
||||||
|
if (def?.context) {
|
||||||
|
contexts = Array.isArray(def.context) ? [...def.context] : [def.context]
|
||||||
|
}
|
||||||
|
if (def?.actions) {
|
||||||
|
contexts.push({
|
||||||
|
type: "action",
|
||||||
|
scope: ContextScopes.Global,
|
||||||
|
|
||||||
|
// Ensure all actions are their verbose object versions
|
||||||
|
actions: def.actions.map(x => (typeof x === "string" ? { type: x } : x)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return contexts
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recurses through the component tree and builds a tree of contexts provided
|
* Recurses through the component tree and builds a tree of contexts provided
|
||||||
* by components.
|
* by components.
|
||||||
|
@ -243,10 +262,9 @@ export const buildContextTree = (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process this component's contexts
|
// Process this component's contexts
|
||||||
const def = componentStore.getDefinition(rootComponent._component)
|
const contexts = getComponentContexts(rootComponent._component)
|
||||||
if (def?.context) {
|
if (contexts.length) {
|
||||||
tree[currentBranch].push(rootComponent._id)
|
tree[currentBranch].push(rootComponent._id)
|
||||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
|
||||||
|
|
||||||
// If we provide local context, start a new branch for our children
|
// If we provide local context, start a new branch for our children
|
||||||
if (contexts.some(context => context.scope === ContextScopes.Local)) {
|
if (contexts.some(context => context.scope === ContextScopes.Local)) {
|
||||||
|
|
|
@ -147,6 +147,15 @@
|
||||||
onOperatorChange(condition, condition.operator)
|
onOperatorChange(condition, condition.operator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onSettingChange = (e, condition) => {
|
||||||
|
const setting = settings.find(x => x.key === e.detail)
|
||||||
|
if (setting?.defaultValue != null) {
|
||||||
|
condition.settingValue = setting.defaultValue
|
||||||
|
} else {
|
||||||
|
delete condition.settingValue
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
@ -189,7 +198,7 @@
|
||||||
<Select
|
<Select
|
||||||
options={settingOptions}
|
options={settingOptions}
|
||||||
bind:value={condition.setting}
|
bind:value={condition.setting}
|
||||||
on:change={() => delete condition.settingValue}
|
on:change={e => onSettingChange(e, condition)}
|
||||||
/>
|
/>
|
||||||
<div>TO</div>
|
<div>TO</div>
|
||||||
{#if definition}
|
{#if definition}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
export let showOnboardingTypeModal
|
export let showOnboardingTypeModal
|
||||||
|
|
||||||
const password = Math.random().toString(36).substring(2, 22)
|
const password = generatePassword(12)
|
||||||
let disabled
|
let disabled
|
||||||
let userGroups = []
|
let userGroups = []
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
{
|
{
|
||||||
email: "",
|
email: "",
|
||||||
role: "appUser",
|
role: "appUser",
|
||||||
password: Math.random().toString(36).substring(2, 22),
|
password: generatePassword(12),
|
||||||
forceResetPassword: true,
|
forceResetPassword: true,
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
|
@ -69,6 +69,14 @@
|
||||||
return userData[index].error == null
|
return userData[index].error == null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generatePassword(length) {
|
||||||
|
const array = new Uint8Array(length)
|
||||||
|
window.crypto.getRandomValues(array)
|
||||||
|
return Array.from(array, byte => byte.toString(36).padStart(2, "0"))
|
||||||
|
.join("")
|
||||||
|
.slice(0, length)
|
||||||
|
}
|
||||||
|
|
||||||
const onConfirm = () => {
|
const onConfirm = () => {
|
||||||
let valid = true
|
let valid = true
|
||||||
userData.forEach((input, index) => {
|
userData.forEach((input, index) => {
|
||||||
|
|
|
@ -216,7 +216,7 @@
|
||||||
const newUser = {
|
const newUser = {
|
||||||
email: email,
|
email: email,
|
||||||
role: usersRole,
|
role: usersRole,
|
||||||
password: Math.random().toString(36).substring(2, 22),
|
password: generatePassword(12),
|
||||||
forceResetPassword: true,
|
forceResetPassword: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,6 +288,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const generatePassword = length => {
|
||||||
|
const array = new Uint8Array(length)
|
||||||
|
window.crypto.getRandomValues(array)
|
||||||
|
return Array.from(array, byte => byte.toString(36).padStart(2, "0"))
|
||||||
|
.join("")
|
||||||
|
.slice(0, length)
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await groups.actions.init()
|
await groups.actions.init()
|
||||||
|
|
|
@ -2,9 +2,16 @@
|
||||||
import { Modal, ModalContent, ProgressBar } from "@budibase/bbui"
|
import { Modal, ModalContent, ProgressBar } from "@budibase/bbui"
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { sleep } from "../../../utils/utils"
|
import { sleep } from "../../../utils/utils"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
const { clipboard, subscribe, copyAllowed, pasteAllowed, selectedCellCount } =
|
const {
|
||||||
getContext("grid")
|
clipboard,
|
||||||
|
subscribe,
|
||||||
|
copyAllowed,
|
||||||
|
pasteAllowed,
|
||||||
|
selectedCellCount,
|
||||||
|
focusedCellAPI,
|
||||||
|
} = getContext("grid")
|
||||||
const duration = 260
|
const duration = 260
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
@ -19,10 +26,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePasteRequest = async () => {
|
const handlePasteRequest = async () => {
|
||||||
|
// If a cell is active then let the native paste action take over
|
||||||
|
if (get(focusedCellAPI)?.isActive()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
progressPercentage = 0
|
progressPercentage = 0
|
||||||
if (!$pasteAllowed) {
|
if (!$pasteAllowed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt if paste will update multiple cells
|
// Prompt if paste will update multiple cells
|
||||||
const multiCellPaste = $selectedCellCount > 1
|
const multiCellPaste = $selectedCellCount > 1
|
||||||
const prompt = $clipboard.multiCellCopy || multiCellPaste
|
const prompt = $clipboard.multiCellCopy || multiCellPaste
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
case "c":
|
case "c":
|
||||||
return handle(() => dispatch("copy"))
|
return handle(() => dispatch("copy"))
|
||||||
case "v":
|
case "v":
|
||||||
return handle(() => dispatch("paste"))
|
return dispatch("paste")
|
||||||
case "Enter":
|
case "Enter":
|
||||||
return handle(() => {
|
return handle(() => {
|
||||||
if ($config.canAddRows) {
|
if ($config.canAddRows) {
|
||||||
|
|
|
@ -1,55 +1,36 @@
|
||||||
import { Datasource, Query } from "@budibase/types"
|
import { Datasource, Query } from "@budibase/types"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import {
|
import { DatabaseName } from "../../integrations/tests/utils"
|
||||||
DatabaseName,
|
|
||||||
getDatasource,
|
|
||||||
knexClient,
|
|
||||||
} from "../../integrations/tests/utils"
|
|
||||||
import { Knex } from "knex"
|
import { Knex } from "knex"
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
|
||||||
|
|
||||||
describe.each(
|
describe.each([
|
||||||
[
|
DatabaseName.POSTGRES,
|
||||||
DatabaseName.POSTGRES,
|
DatabaseName.MYSQL,
|
||||||
DatabaseName.MYSQL,
|
DatabaseName.SQL_SERVER,
|
||||||
DatabaseName.SQL_SERVER,
|
DatabaseName.MARIADB,
|
||||||
DatabaseName.MARIADB,
|
DatabaseName.ORACLE,
|
||||||
DatabaseName.ORACLE,
|
])("execute query action (%s)", name => {
|
||||||
].map(name => [name, getDatasource(name)])
|
|
||||||
)("execute query action (%s)", (_, dsProvider) => {
|
|
||||||
let tableName: string
|
let tableName: string
|
||||||
let client: Knex
|
let client: Knex
|
||||||
let datasource: Datasource
|
let datasource: Datasource
|
||||||
let query: Query
|
let query: Query
|
||||||
let config = setup.getConfig()
|
const config = setup.getConfig()
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await config.init()
|
await config.init()
|
||||||
|
|
||||||
const ds = await dsProvider
|
const testSetup = await setup.setupTestDatasource(config, name)
|
||||||
datasource = await config.api.datasource.create(ds)
|
datasource = testSetup.datasource
|
||||||
client = await knexClient(ds)
|
client = testSetup.client
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
tableName = generator.guid()
|
tableName = await setup.createTestTable(client, {
|
||||||
await client.schema.createTable(tableName, table => {
|
a: { type: "string" },
|
||||||
table.string("a")
|
b: { type: "number" },
|
||||||
table.integer("b")
|
|
||||||
})
|
|
||||||
await client(tableName).insert({ a: "string", b: 1 })
|
|
||||||
query = await config.api.query.save({
|
|
||||||
name: "test query",
|
|
||||||
datasourceId: datasource._id!,
|
|
||||||
parameters: [],
|
|
||||||
fields: {
|
|
||||||
sql: client(tableName).select("*").toSQL().toNative().sql,
|
|
||||||
},
|
|
||||||
transformer: "",
|
|
||||||
schema: {},
|
|
||||||
readable: true,
|
|
||||||
queryVerb: "read",
|
|
||||||
})
|
})
|
||||||
|
await setup.insertTestData(client, tableName, [{ a: "string", b: 1 }])
|
||||||
|
query = await setup.saveTestQuery(config, client, tableName, datasource)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
import * as automation from "../../index"
|
import * as automation from "../../index"
|
||||||
import * as setup from "../utilities"
|
import * as setup from "../utilities"
|
||||||
import { Table, LoopStepType } from "@budibase/types"
|
import {
|
||||||
|
Table,
|
||||||
|
LoopStepType,
|
||||||
|
CreateRowStepOutputs,
|
||||||
|
ServerLogStepOutputs,
|
||||||
|
FieldType,
|
||||||
|
} from "@budibase/types"
|
||||||
import { createAutomationBuilder } from "../utilities/AutomationBuilder"
|
import { createAutomationBuilder } from "../utilities/AutomationBuilder"
|
||||||
|
import { DatabaseName } from "../../../integrations/tests/utils"
|
||||||
|
|
||||||
describe("Automation Scenarios", () => {
|
describe("Automation Scenarios", () => {
|
||||||
let config = setup.getConfig(),
|
let config = setup.getConfig(),
|
||||||
|
@ -63,6 +70,72 @@ describe("Automation Scenarios", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should run an automation where a loop is successfully run twice", async () => {
|
||||||
|
const builder = createAutomationBuilder({
|
||||||
|
name: "Test Trigger with Loop and Create Row",
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = await builder
|
||||||
|
.rowSaved(
|
||||||
|
{ tableId: table._id! },
|
||||||
|
{
|
||||||
|
row: {
|
||||||
|
name: "Trigger Row",
|
||||||
|
description: "This row triggers the automation",
|
||||||
|
},
|
||||||
|
id: "1234",
|
||||||
|
revision: "1",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.loop({
|
||||||
|
option: LoopStepType.ARRAY,
|
||||||
|
binding: [1, 2, 3],
|
||||||
|
})
|
||||||
|
.createRow({
|
||||||
|
row: {
|
||||||
|
name: "Item {{ loop.currentItem }}",
|
||||||
|
description: "Created from loop",
|
||||||
|
tableId: table._id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.loop({
|
||||||
|
option: LoopStepType.STRING,
|
||||||
|
binding: "Message 1,Message 2,Message 3",
|
||||||
|
})
|
||||||
|
.serverLog({ text: "{{loop.currentItem}}" })
|
||||||
|
.run()
|
||||||
|
|
||||||
|
expect(results.trigger).toBeDefined()
|
||||||
|
expect(results.steps).toHaveLength(2)
|
||||||
|
|
||||||
|
expect(results.steps[0].outputs.iterations).toBe(3)
|
||||||
|
expect(results.steps[0].outputs.items).toHaveLength(3)
|
||||||
|
|
||||||
|
results.steps[0].outputs.items.forEach(
|
||||||
|
(output: CreateRowStepOutputs, index: number) => {
|
||||||
|
expect(output).toMatchObject({
|
||||||
|
success: true,
|
||||||
|
row: {
|
||||||
|
name: `Item ${index + 1}`,
|
||||||
|
description: "Created from loop",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(results.steps[1].outputs.iterations).toBe(3)
|
||||||
|
expect(results.steps[1].outputs.items).toHaveLength(3)
|
||||||
|
|
||||||
|
results.steps[1].outputs.items.forEach(
|
||||||
|
(output: ServerLogStepOutputs, index: number) => {
|
||||||
|
expect(output).toMatchObject({
|
||||||
|
success: true,
|
||||||
|
})
|
||||||
|
expect(output.message).toContain(`Message ${index + 1}`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Row Automations", () => {
|
describe("Row Automations", () => {
|
||||||
|
@ -157,4 +230,94 @@ describe("Automation Scenarios", () => {
|
||||||
expect(results.steps[1].outputs.success).toBeTruthy()
|
expect(results.steps[1].outputs.success).toBeTruthy()
|
||||||
expect(results.steps[2].outputs.rows).toHaveLength(1)
|
expect(results.steps[2].outputs.rows).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should query an external database for some data then insert than into an internal table", async () => {
|
||||||
|
const { datasource, client } = await setup.setupTestDatasource(
|
||||||
|
config,
|
||||||
|
DatabaseName.MYSQL
|
||||||
|
)
|
||||||
|
|
||||||
|
const newTable = await config.createTable({
|
||||||
|
name: "table",
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
name: "age",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableName = await setup.createTestTable(client, {
|
||||||
|
name: { type: "string" },
|
||||||
|
age: { type: "number" },
|
||||||
|
})
|
||||||
|
|
||||||
|
const rows = [
|
||||||
|
{ name: "Joe", age: 20 },
|
||||||
|
{ name: "Bob", age: 25 },
|
||||||
|
{ name: "Paul", age: 30 },
|
||||||
|
]
|
||||||
|
|
||||||
|
await setup.insertTestData(client, tableName, rows)
|
||||||
|
|
||||||
|
const query = await setup.saveTestQuery(
|
||||||
|
config,
|
||||||
|
client,
|
||||||
|
tableName,
|
||||||
|
datasource
|
||||||
|
)
|
||||||
|
|
||||||
|
const builder = createAutomationBuilder({
|
||||||
|
name: "Test external query and save",
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = await builder
|
||||||
|
.appAction({
|
||||||
|
fields: {},
|
||||||
|
})
|
||||||
|
.executeQuery({
|
||||||
|
query: {
|
||||||
|
queryId: query._id!,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.loop({
|
||||||
|
option: LoopStepType.ARRAY,
|
||||||
|
binding: "{{ steps.1.response }}",
|
||||||
|
})
|
||||||
|
.createRow({
|
||||||
|
row: {
|
||||||
|
name: "{{ loop.currentItem.name }}",
|
||||||
|
age: "{{ loop.currentItem.age }}",
|
||||||
|
tableId: newTable._id!,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.queryRows({
|
||||||
|
tableId: newTable._id!,
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
|
||||||
|
expect(results.steps).toHaveLength(3)
|
||||||
|
|
||||||
|
expect(results.steps[1].outputs.iterations).toBe(3)
|
||||||
|
expect(results.steps[1].outputs.items).toHaveLength(3)
|
||||||
|
|
||||||
|
expect(results.steps[2].outputs.rows).toHaveLength(3)
|
||||||
|
|
||||||
|
rows.forEach(expectedRow => {
|
||||||
|
expect(results.steps[2].outputs.rows).toEqual(
|
||||||
|
expect.arrayContaining([expect.objectContaining(expectedRow)])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {
|
||||||
SmtpEmailStepInputs,
|
SmtpEmailStepInputs,
|
||||||
ExecuteQueryStepInputs,
|
ExecuteQueryStepInputs,
|
||||||
QueryRowsStepInputs,
|
QueryRowsStepInputs,
|
||||||
|
ServerLogStepInputs,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {} from "../../steps/loop"
|
import {} from "../../steps/loop"
|
||||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||||
|
@ -119,6 +120,10 @@ class AutomationBuilder {
|
||||||
return this.step(BUILTIN_ACTION_DEFINITIONS.LOOP, inputs)
|
return this.step(BUILTIN_ACTION_DEFINITIONS.LOOP, inputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serverLog(input: ServerLogStepInputs): this {
|
||||||
|
return this.step(BUILTIN_ACTION_DEFINITIONS.SERVER_LOG, input)
|
||||||
|
}
|
||||||
|
|
||||||
private trigger<T extends { [key: string]: any }>(
|
private trigger<T extends { [key: string]: any }>(
|
||||||
triggerSchema: AutomationTriggerSchema,
|
triggerSchema: AutomationTriggerSchema,
|
||||||
inputs?: T,
|
inputs?: T,
|
||||||
|
|
|
@ -3,7 +3,14 @@ import { context } from "@budibase/backend-core"
|
||||||
import { BUILTIN_ACTION_DEFINITIONS, getAction } from "../../actions"
|
import { BUILTIN_ACTION_DEFINITIONS, getAction } from "../../actions"
|
||||||
import emitter from "../../../events/index"
|
import emitter from "../../../events/index"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import { AutomationActionStepId } from "@budibase/types"
|
import { AutomationActionStepId, Datasource } from "@budibase/types"
|
||||||
|
import { Knex } from "knex"
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
import {
|
||||||
|
getDatasource,
|
||||||
|
knexClient,
|
||||||
|
DatabaseName,
|
||||||
|
} from "../../../integrations/tests/utils"
|
||||||
|
|
||||||
let config: TestConfig
|
let config: TestConfig
|
||||||
|
|
||||||
|
@ -57,5 +64,58 @@ export async function runStep(stepId: string, inputs: any, stepContext?: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createTestTable(client: Knex, schema: any) {
|
||||||
|
const tableName = generator.guid()
|
||||||
|
await client.schema.createTable(tableName, table => {
|
||||||
|
for (const fieldName in schema) {
|
||||||
|
const field = schema[fieldName]
|
||||||
|
if (field.type === "string") {
|
||||||
|
table.string(fieldName)
|
||||||
|
} else if (field.type === "number") {
|
||||||
|
table.integer(fieldName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return tableName
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function insertTestData(
|
||||||
|
client: Knex,
|
||||||
|
tableName: string,
|
||||||
|
rows: any[]
|
||||||
|
) {
|
||||||
|
await client(tableName).insert(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveTestQuery(
|
||||||
|
config: TestConfig,
|
||||||
|
client: Knex,
|
||||||
|
tableName: string,
|
||||||
|
datasource: Datasource
|
||||||
|
) {
|
||||||
|
return await config.api.query.save({
|
||||||
|
name: "test query",
|
||||||
|
datasourceId: datasource._id!,
|
||||||
|
parameters: [],
|
||||||
|
fields: {
|
||||||
|
sql: client(tableName).select("*").toSQL().toNative().sql,
|
||||||
|
},
|
||||||
|
transformer: "",
|
||||||
|
schema: {},
|
||||||
|
readable: true,
|
||||||
|
queryVerb: "read",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupTestDatasource(
|
||||||
|
config: TestConfig,
|
||||||
|
dbName: DatabaseName
|
||||||
|
) {
|
||||||
|
const db = await getDatasource(dbName)
|
||||||
|
const datasource = await config.api.datasource.create(db)
|
||||||
|
const client = await knexClient(db)
|
||||||
|
return { datasource, client }
|
||||||
|
}
|
||||||
|
|
||||||
export const apiKey = "test"
|
export const apiKey = "test"
|
||||||
export const actions = BUILTIN_ACTION_DEFINITIONS
|
export const actions = BUILTIN_ACTION_DEFINITIONS
|
||||||
|
|
|
@ -98,6 +98,24 @@ export type ActionImplementations<T extends Hosting> = {
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
|
|
||||||
|
export type AutomationStepOutputs =
|
||||||
|
| CollectStepOutputs
|
||||||
|
| CreateRowStepOutputs
|
||||||
|
| DelayStepOutputs
|
||||||
|
| DeleteRowStepOutputs
|
||||||
|
| ExecuteQueryStepOutputs
|
||||||
|
| ExecuteScriptStepOutputs
|
||||||
|
| FilterStepOutputs
|
||||||
|
| QueryRowsStepOutputs
|
||||||
|
| BaseAutomationOutputs
|
||||||
|
| BashStepOutputs
|
||||||
|
| ExternalAppStepOutputs
|
||||||
|
| OpenAIStepOutputs
|
||||||
|
| ServerLogStepOutputs
|
||||||
|
| TriggerAutomationStepOutputs
|
||||||
|
| UpdateRowStepOutputs
|
||||||
|
| ZapierStepOutputs
|
||||||
|
|
||||||
export type BaseAutomationOutputs = {
|
export type BaseAutomationOutputs = {
|
||||||
success?: boolean
|
success?: boolean
|
||||||
response?: {
|
response?: {
|
||||||
|
@ -199,7 +217,7 @@ export type LoopStepInputs = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LoopStepOutputs = {
|
export type LoopStepOutputs = {
|
||||||
items: string
|
items: AutomationStepOutputs[]
|
||||||
success: boolean
|
success: boolean
|
||||||
iterations: number
|
iterations: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,14 @@ import { BpmStatusKey, BpmStatusValue } from "@budibase/shared-core"
|
||||||
|
|
||||||
const MAX_USERS_UPLOAD_LIMIT = 1000
|
const MAX_USERS_UPLOAD_LIMIT = 1000
|
||||||
|
|
||||||
|
const generatePassword = (length: number) => {
|
||||||
|
const array = new Uint8Array(length)
|
||||||
|
crypto.getRandomValues(array)
|
||||||
|
return Array.from(array, byte => byte.toString(36).padStart(2, "0"))
|
||||||
|
.join("")
|
||||||
|
.slice(0, length)
|
||||||
|
}
|
||||||
|
|
||||||
export const save = async (ctx: UserCtx<User, SaveUserResponse>) => {
|
export const save = async (ctx: UserCtx<User, SaveUserResponse>) => {
|
||||||
try {
|
try {
|
||||||
const currentUserId = ctx.user?._id
|
const currentUserId = ctx.user?._id
|
||||||
|
@ -296,7 +304,7 @@ export const onboardUsers = async (
|
||||||
|
|
||||||
let createdPasswords: Record<string, string> = {}
|
let createdPasswords: Record<string, string> = {}
|
||||||
const users: User[] = ctx.request.body.map(invite => {
|
const users: User[] = ctx.request.body.map(invite => {
|
||||||
let password = Math.random().toString(36).substring(2, 22)
|
const password = generatePassword(12)
|
||||||
createdPasswords[invite.email] = password
|
createdPasswords[invite.email] = password
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
Loading…
Reference in New Issue