Merge branch 'master' of github.com:Budibase/budibase into default-app-design
This commit is contained in:
commit
dab42d3d0e
packages
bbui/src
builder/src
components
automation
AutomationBuilder/FlowChart
AutomationPanel
SetupPanel
backend
common
CodeEditor
bindings
design/settings/controls
ButtonActionEditor
FilterEditor
integration
pages/builder/app/[application]
automation
data/datasource/[datasourceId]/_components/panels/Queries
stores/builder
client/src
frontend-core/src/components
server/src
api
automations
integrations
types/src
|
@ -1,21 +1,21 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
import TextArea from "./Core/TextArea.svelte"
|
import TextArea from "./Core/TextArea.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let value = null
|
export let value: string | undefined = undefined
|
||||||
export let label = null
|
export let label: string | undefined = undefined
|
||||||
export let labelPosition = "above"
|
export let labelPosition: string = "above"
|
||||||
export let placeholder = null
|
export let placeholder: string | undefined = undefined
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let error = null
|
export let error: string | undefined = undefined
|
||||||
export let getCaretPosition = null
|
export let getCaretPosition: any = undefined
|
||||||
export let height = null
|
export let height: string | number | undefined = undefined
|
||||||
export let minHeight = null
|
export let minHeight: string | number | undefined = undefined
|
||||||
export let helpText = null
|
export let helpText: string | undefined = undefined
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = (e: any) => {
|
||||||
value = e.detail
|
value = e.detail
|
||||||
dispatch("change", e.detail)
|
dispatch("change", e.detail)
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,6 @@
|
||||||
<Field {helpText} {label} {labelPosition} {error}>
|
<Field {helpText} {label} {labelPosition} {error}>
|
||||||
<TextArea
|
<TextArea
|
||||||
bind:getCaretPosition
|
bind:getCaretPosition
|
||||||
{error}
|
|
||||||
{disabled}
|
{disabled}
|
||||||
{value}
|
{value}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import "@spectrum-css/inlinealert/dist/index-vars.css"
|
import "@spectrum-css/inlinealert/dist/index-vars.css"
|
||||||
import Button from "../Button/Button.svelte"
|
import Button from "../Button/Button.svelte"
|
||||||
|
import Icon from "../Icon/Icon.svelte"
|
||||||
|
|
||||||
export let type = "info"
|
export let type = "info"
|
||||||
export let header = ""
|
export let header = ""
|
||||||
|
@ -8,6 +9,8 @@
|
||||||
export let onConfirm = undefined
|
export let onConfirm = undefined
|
||||||
export let buttonText = ""
|
export let buttonText = ""
|
||||||
export let cta = false
|
export let cta = false
|
||||||
|
export let link = ""
|
||||||
|
export let linkText = ""
|
||||||
|
|
||||||
$: icon = selectIcon(type)
|
$: icon = selectIcon(type)
|
||||||
// if newlines used, convert them to different elements
|
// if newlines used, convert them to different elements
|
||||||
|
@ -49,6 +52,19 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if link && linkText}
|
||||||
|
<div id="docs-link">
|
||||||
|
<a
|
||||||
|
href={link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="docs-link"
|
||||||
|
>
|
||||||
|
{linkText}
|
||||||
|
<Icon name="LinkOut" size="XS" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -64,4 +80,21 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#docs-link {
|
||||||
|
padding-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#docs-link > * {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let icon = ""
|
export let icon = ""
|
||||||
export let id
|
export let id = undefined
|
||||||
export let href = "#"
|
export let href = "#"
|
||||||
export let link = false
|
export let link = false
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,12 @@
|
||||||
import AutomationBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte"
|
import AutomationBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte"
|
||||||
import FlowItemHeader from "./FlowItemHeader.svelte"
|
import FlowItemHeader from "./FlowItemHeader.svelte"
|
||||||
import FlowItemActions from "./FlowItemActions.svelte"
|
import FlowItemActions from "./FlowItemActions.svelte"
|
||||||
import { automationStore, selectedAutomation } from "@/stores/builder"
|
import {
|
||||||
import { QueryUtils, Utils } from "@budibase/frontend-core"
|
automationStore,
|
||||||
|
selectedAutomation,
|
||||||
|
evaluationContext,
|
||||||
|
} from "@/stores/builder"
|
||||||
|
import { QueryUtils, Utils, memo } from "@budibase/frontend-core"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
import DragZone from "./DragZone.svelte"
|
import DragZone from "./DragZone.svelte"
|
||||||
|
@ -34,11 +38,14 @@
|
||||||
export let automation
|
export let automation
|
||||||
|
|
||||||
const view = getContext("draggableView")
|
const view = getContext("draggableView")
|
||||||
|
const memoContext = memo({})
|
||||||
|
|
||||||
let drawer
|
let drawer
|
||||||
let open = true
|
let open = true
|
||||||
let confirmDeleteModal
|
let confirmDeleteModal
|
||||||
|
|
||||||
|
$: memoContext.set($evaluationContext)
|
||||||
|
|
||||||
$: branch = step.inputs?.branches?.[branchIdx]
|
$: branch = step.inputs?.branches?.[branchIdx]
|
||||||
$: editableConditionUI = branch.conditionUI || {}
|
$: editableConditionUI = branch.conditionUI || {}
|
||||||
|
|
||||||
|
@ -100,6 +107,7 @@
|
||||||
allowOnEmpty={false}
|
allowOnEmpty={false}
|
||||||
builderType={"condition"}
|
builderType={"condition"}
|
||||||
docsURL={null}
|
docsURL={null}
|
||||||
|
evaluationContext={$memoContext}
|
||||||
/>
|
/>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
|
@ -32,7 +32,6 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
$: groupedAutomations = groupAutomations(filteredAutomations)
|
$: groupedAutomations = groupAutomations(filteredAutomations)
|
||||||
|
|
||||||
$: showNoResults = searchString && !filteredAutomations.length
|
$: showNoResults = searchString && !filteredAutomations.length
|
||||||
|
|
||||||
const groupAutomations = automations => {
|
const groupAutomations = automations => {
|
||||||
|
@ -41,7 +40,6 @@
|
||||||
for (let auto of automations) {
|
for (let auto of automations) {
|
||||||
let category = null
|
let category = null
|
||||||
let dataTrigger = false
|
let dataTrigger = false
|
||||||
|
|
||||||
// Group by datasource if possible
|
// Group by datasource if possible
|
||||||
if (dsTriggers.includes(auto.definition?.trigger?.stepId)) {
|
if (dsTriggers.includes(auto.definition?.trigger?.stepId)) {
|
||||||
if (auto.definition.trigger.inputs?.tableId) {
|
if (auto.definition.trigger.inputs?.tableId) {
|
||||||
|
@ -97,7 +95,10 @@
|
||||||
{triggerGroup?.name}
|
{triggerGroup?.name}
|
||||||
</div>
|
</div>
|
||||||
{#each triggerGroup.entries as automation}
|
{#each triggerGroup.entries as automation}
|
||||||
<AutomationNavItem {automation} icon={triggerGroup.icon} />
|
<AutomationNavItem
|
||||||
|
{automation}
|
||||||
|
icon={automation?.definition?.trigger?.icon}
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import { automationStore, tables } from "@/stores/builder"
|
import { automationStore, tables, evaluationContext } from "@/stores/builder"
|
||||||
import { environment } from "@/stores/portal"
|
import { environment } from "@/stores/portal"
|
||||||
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
||||||
import {
|
import {
|
||||||
|
@ -70,6 +70,7 @@
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import PropField from "./PropField.svelte"
|
import PropField from "./PropField.svelte"
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils } from "@budibase/shared-core"
|
||||||
|
import DrawerBindableCodeEditorField from "@/components/common/bindings/DrawerBindableCodeEditorField.svelte"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import InfoDisplay from "@/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
import InfoDisplay from "@/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
||||||
|
|
||||||
|
@ -84,6 +85,7 @@
|
||||||
|
|
||||||
// Stop unnecessary rendering
|
// Stop unnecessary rendering
|
||||||
const memoBlock = memo(block)
|
const memoBlock = memo(block)
|
||||||
|
const memoContext = memo({})
|
||||||
|
|
||||||
const rowTriggers = [
|
const rowTriggers = [
|
||||||
TriggerStepID.ROW_UPDATED,
|
TriggerStepID.ROW_UPDATED,
|
||||||
|
@ -109,6 +111,7 @@
|
||||||
let selectedRow
|
let selectedRow
|
||||||
|
|
||||||
$: memoBlock.set(block)
|
$: memoBlock.set(block)
|
||||||
|
$: memoContext.set($evaluationContext)
|
||||||
|
|
||||||
$: filters = lookForFilters(schemaProperties)
|
$: filters = lookForFilters(schemaProperties)
|
||||||
$: filterCount =
|
$: filterCount =
|
||||||
|
@ -250,7 +253,6 @@
|
||||||
onChange({ ["revision"]: e.detail })
|
onChange({ ["revision"]: e.detail })
|
||||||
},
|
},
|
||||||
updateOnChange: false,
|
updateOnChange: false,
|
||||||
forceModal: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -278,7 +280,6 @@
|
||||||
onChange({ [rowIdentifier]: e.detail })
|
onChange({ [rowIdentifier]: e.detail })
|
||||||
},
|
},
|
||||||
updateOnChange: false,
|
updateOnChange: false,
|
||||||
forceModal: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -569,6 +570,10 @@
|
||||||
...update,
|
...update,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!updatedAutomation) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Exclude default or invalid data from the test data
|
// Exclude default or invalid data from the test data
|
||||||
let updatedFields = {}
|
let updatedFields = {}
|
||||||
for (const key of Object.keys(block?.inputs?.fields || {})) {
|
for (const key of Object.keys(block?.inputs?.fields || {})) {
|
||||||
|
@ -640,7 +645,7 @@
|
||||||
...newTestData,
|
...newTestData,
|
||||||
body: {
|
body: {
|
||||||
...update,
|
...update,
|
||||||
...automation.testData?.body,
|
...(automation?.testData?.body || {}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -770,6 +775,7 @@
|
||||||
{...config.props}
|
{...config.props}
|
||||||
{bindings}
|
{bindings}
|
||||||
on:change={config.props.onChange}
|
on:change={config.props.onChange}
|
||||||
|
context={$memoContext}
|
||||||
bind:searchTerm={rowSearchTerm}
|
bind:searchTerm={rowSearchTerm}
|
||||||
/>
|
/>
|
||||||
</PropField>
|
</PropField>
|
||||||
|
@ -779,6 +785,7 @@
|
||||||
{...config.props}
|
{...config.props}
|
||||||
{bindings}
|
{bindings}
|
||||||
on:change={config.props.onChange}
|
on:change={config.props.onChange}
|
||||||
|
context={$memoContext}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -903,6 +910,7 @@
|
||||||
: "Add signature"}
|
: "Add signature"}
|
||||||
keyPlaceholder={"URL"}
|
keyPlaceholder={"URL"}
|
||||||
valuePlaceholder={"Filename"}
|
valuePlaceholder={"Filename"}
|
||||||
|
context={$memoContext}
|
||||||
/>
|
/>
|
||||||
{:else if isTestModal}
|
{:else if isTestModal}
|
||||||
<ModalBindableInput
|
<ModalBindableInput
|
||||||
|
@ -927,6 +935,7 @@
|
||||||
? queryLimit
|
? queryLimit
|
||||||
: ""}
|
: ""}
|
||||||
drawerLeft="260px"
|
drawerLeft="260px"
|
||||||
|
context={$memoContext}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -956,6 +965,7 @@
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
showFilterEmptyDropdown={!rowTriggers.includes(stepId)}
|
showFilterEmptyDropdown={!rowTriggers.includes(stepId)}
|
||||||
on:change={e => (tempFilters = e.detail)}
|
on:change={e => (tempFilters = e.detail)}
|
||||||
|
evaluationContext={$memoContext}
|
||||||
/>
|
/>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
@ -998,7 +1008,19 @@
|
||||||
on:change={e => onChange({ [key]: e.detail })}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "code"}
|
{:else if value.customType === "code" && stepId === ActionStepID.EXECUTE_SCRIPT_V2}
|
||||||
|
<div class="scriptv2-wrapper">
|
||||||
|
<DrawerBindableCodeEditorField
|
||||||
|
{bindings}
|
||||||
|
{schema}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
|
context={$memoContext}
|
||||||
|
value={inputData[key]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else if value.customType === "code" && stepId === ActionStepID.EXECUTE_SCRIPT}
|
||||||
|
<!-- DEPRECATED -->
|
||||||
<CodeEditorModal
|
<CodeEditorModal
|
||||||
on:hide={() => {
|
on:hide={() => {
|
||||||
// Push any pending changes when the window closes
|
// Push any pending changes when the window closes
|
||||||
|
@ -1080,6 +1102,7 @@
|
||||||
? queryLimit
|
? queryLimit
|
||||||
: ""}
|
: ""}
|
||||||
drawerLeft="260px"
|
drawerLeft="260px"
|
||||||
|
context={$memoContext}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
export let meta
|
export let meta
|
||||||
export let bindings
|
export let bindings
|
||||||
export let isTestModal
|
export let isTestModal
|
||||||
|
export let context = {}
|
||||||
|
|
||||||
const typeToField = Object.values(FIELDS).reduce((acc, field) => {
|
const typeToField = Object.values(FIELDS).reduce((acc, field) => {
|
||||||
acc[field.type] = field
|
acc[field.type] = field
|
||||||
|
@ -58,7 +59,7 @@
|
||||||
|
|
||||||
$: parsedBindings = bindings.map(binding => {
|
$: parsedBindings = bindings.map(binding => {
|
||||||
let clone = Object.assign({}, binding)
|
let clone = Object.assign({}, binding)
|
||||||
clone.icon = "ShareAndroid"
|
clone.icon = clone.icon ?? "ShareAndroid"
|
||||||
return clone
|
return clone
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -258,6 +259,7 @@
|
||||||
fields: editableFields,
|
fields: editableFields,
|
||||||
}}
|
}}
|
||||||
{onChange}
|
{onChange}
|
||||||
|
{context}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<DrawerBindableSlot
|
<DrawerBindableSlot
|
||||||
|
@ -276,6 +278,7 @@
|
||||||
allowJS={true}
|
allowJS={true}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
drawerLeft="260px"
|
drawerLeft="260px"
|
||||||
|
{context}
|
||||||
>
|
>
|
||||||
<RowSelectorTypes
|
<RowSelectorTypes
|
||||||
{isTestModal}
|
{isTestModal}
|
||||||
|
@ -286,6 +289,7 @@
|
||||||
meta={{
|
meta={{
|
||||||
fields: editableFields,
|
fields: editableFields,
|
||||||
}}
|
}}
|
||||||
|
{context}
|
||||||
onChange={change => onChange(change)}
|
onChange={change => onChange(change)}
|
||||||
/>
|
/>
|
||||||
</DrawerBindableSlot>
|
</DrawerBindableSlot>
|
||||||
|
|
|
@ -25,12 +25,13 @@
|
||||||
export let meta
|
export let meta
|
||||||
export let bindings
|
export let bindings
|
||||||
export let isTestModal
|
export let isTestModal
|
||||||
|
export let context
|
||||||
|
|
||||||
$: fieldData = value[field]
|
$: fieldData = value[field]
|
||||||
|
|
||||||
$: parsedBindings = bindings.map(binding => {
|
$: parsedBindings = bindings.map(binding => {
|
||||||
let clone = Object.assign({}, binding)
|
let clone = Object.assign({}, binding)
|
||||||
clone.icon = "ShareAndroid"
|
clone.icon = clone.icon ?? "ShareAndroid"
|
||||||
return clone
|
return clone
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -232,6 +233,7 @@
|
||||||
actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE ||
|
actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
schema.type === FieldType.SIGNATURE_SINGLE) &&
|
schema.type === FieldType.SIGNATURE_SINGLE) &&
|
||||||
fieldData}
|
fieldData}
|
||||||
|
{context}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { Input, Select, Button } from "@budibase/bbui"
|
import { Input, Select, Button } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { memo } from "@budibase/frontend-core"
|
||||||
const dispatch = createEventDispatcher()
|
import { generate } from "shortid"
|
||||||
|
|
||||||
export let value = {}
|
export let value = {}
|
||||||
|
|
||||||
$: fieldsArray = value
|
|
||||||
? Object.entries(value).map(([name, type]) => ({
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
}))
|
|
||||||
: []
|
|
||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
{
|
{
|
||||||
label: "Text",
|
label: "Text",
|
||||||
|
@ -36,16 +29,42 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const memoValue = memo({ data: {} })
|
||||||
|
|
||||||
|
$: memoValue.set({ data: value })
|
||||||
|
|
||||||
|
$: fieldsArray = $memoValue.data
|
||||||
|
? Object.entries($memoValue.data).map(([name, type]) => ({
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
id: generate(),
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
|
||||||
function addField() {
|
function addField() {
|
||||||
const newValue = { ...value }
|
const newValue = { ...$memoValue.data }
|
||||||
newValue[""] = "string"
|
newValue[""] = "string"
|
||||||
dispatch("change", newValue)
|
fieldsArray = [...fieldsArray, { name: "", type: "string", id: generate() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeField(name) {
|
function removeField(idx) {
|
||||||
const newValues = { ...value }
|
const entries = [...fieldsArray]
|
||||||
delete newValues[name]
|
|
||||||
dispatch("change", newValues)
|
// Remove empty field
|
||||||
|
if (!entries[idx]?.name) {
|
||||||
|
fieldsArray.splice(idx, 1)
|
||||||
|
fieldsArray = [...fieldsArray]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.splice(idx, 1)
|
||||||
|
|
||||||
|
const update = entries.reduce((newVals, current) => {
|
||||||
|
newVals[current.name.trim()] = current.type
|
||||||
|
return newVals
|
||||||
|
}, {})
|
||||||
|
dispatch("change", update)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldNameChanged = originalName => e => {
|
const fieldNameChanged = originalName => e => {
|
||||||
|
@ -57,11 +76,16 @@
|
||||||
} else {
|
} else {
|
||||||
entries = entries.filter(f => f.name !== originalName)
|
entries = entries.filter(f => f.name !== originalName)
|
||||||
}
|
}
|
||||||
value = entries.reduce((newVals, current) => {
|
|
||||||
|
const update = entries
|
||||||
|
.filter(entry => entry.name)
|
||||||
|
.reduce((newVals, current) => {
|
||||||
newVals[current.name.trim()] = current.type
|
newVals[current.name.trim()] = current.type
|
||||||
return newVals
|
return newVals
|
||||||
}, {})
|
}, {})
|
||||||
dispatch("change", value)
|
if (Object.keys(update).length) {
|
||||||
|
dispatch("change", update)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -69,7 +93,7 @@
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="spacer" />
|
<div class="spacer" />
|
||||||
{#each fieldsArray as field}
|
{#each fieldsArray as field, idx (field.id)}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Input
|
<Input
|
||||||
value={field.name}
|
value={field.name}
|
||||||
|
@ -88,7 +112,9 @@
|
||||||
/>
|
/>
|
||||||
<i
|
<i
|
||||||
class="remove-field ri-delete-bin-line"
|
class="remove-field ri-delete-bin-line"
|
||||||
on:click={() => removeField(field.name)}
|
on:click={() => {
|
||||||
|
removeField(idx)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -115,4 +141,12 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.remove-field {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-field:hover {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,132 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import {
|
|
||||||
keepOpen,
|
|
||||||
ModalContent,
|
|
||||||
notifications,
|
|
||||||
Body,
|
|
||||||
Layout,
|
|
||||||
Tabs,
|
|
||||||
Tab,
|
|
||||||
Heading,
|
|
||||||
TextArea,
|
|
||||||
Dropzone,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { datasources, queries } from "@/stores/builder"
|
|
||||||
import { writable } from "svelte/store"
|
|
||||||
|
|
||||||
export let navigateDatasource = false
|
|
||||||
export let datasourceId
|
|
||||||
export let createDatasource = false
|
|
||||||
export let onCancel
|
|
||||||
|
|
||||||
const data = writable({
|
|
||||||
url: "",
|
|
||||||
raw: "",
|
|
||||||
file: undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
let lastTouched = "url"
|
|
||||||
|
|
||||||
const getData = async () => {
|
|
||||||
let dataString
|
|
||||||
|
|
||||||
// parse the file into memory and send as string
|
|
||||||
if (lastTouched === "file") {
|
|
||||||
dataString = await $data.file.text()
|
|
||||||
} else if (lastTouched === "url") {
|
|
||||||
const response = await fetch($data.url)
|
|
||||||
dataString = await response.text()
|
|
||||||
} else if (lastTouched === "raw") {
|
|
||||||
dataString = $data.raw
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataString
|
|
||||||
}
|
|
||||||
|
|
||||||
async function importQueries() {
|
|
||||||
try {
|
|
||||||
const dataString = await getData()
|
|
||||||
|
|
||||||
if (!datasourceId && !createDatasource) {
|
|
||||||
throw new Error("No datasource id")
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = {
|
|
||||||
data: dataString,
|
|
||||||
datasourceId,
|
|
||||||
}
|
|
||||||
|
|
||||||
const importResult = await queries.import(body)
|
|
||||||
if (!datasourceId) {
|
|
||||||
datasourceId = importResult.datasourceId
|
|
||||||
}
|
|
||||||
|
|
||||||
// reload
|
|
||||||
await datasources.fetch()
|
|
||||||
await queries.fetch()
|
|
||||||
|
|
||||||
if (navigateDatasource) {
|
|
||||||
$goto(`./datasource/${datasourceId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
notifications.success(`Imported successfully.`)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error importing queries")
|
|
||||||
return keepOpen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ModalContent
|
|
||||||
onConfirm={() => importQueries()}
|
|
||||||
{onCancel}
|
|
||||||
confirmText={"Import"}
|
|
||||||
cancelText="Back"
|
|
||||||
size="L"
|
|
||||||
>
|
|
||||||
<Layout noPadding>
|
|
||||||
<Heading size="S">Import</Heading>
|
|
||||||
<Body size="XS"
|
|
||||||
>Import your rest collection using one of the options below</Body
|
|
||||||
>
|
|
||||||
<Tabs selected="File">
|
|
||||||
<!-- Commenting until nginx csp issue resolved -->
|
|
||||||
<!-- <Tab title="Link">
|
|
||||||
<Input
|
|
||||||
bind:value={$data.url}
|
|
||||||
on:change={() => (lastTouched = "url")}
|
|
||||||
label="Enter a URL"
|
|
||||||
placeholder="e.g. https://petstore.swagger.io/v2/swagger.json"
|
|
||||||
/>
|
|
||||||
</Tab> -->
|
|
||||||
<Tab title="File">
|
|
||||||
<Dropzone
|
|
||||||
gallery={false}
|
|
||||||
value={$data.file ? [$data.file] : []}
|
|
||||||
on:change={e => {
|
|
||||||
$data.file = e.detail?.[0]
|
|
||||||
lastTouched = "file"
|
|
||||||
}}
|
|
||||||
fileTags={[
|
|
||||||
"OpenAPI 3.0",
|
|
||||||
"OpenAPI 2.0",
|
|
||||||
"Swagger 2.0",
|
|
||||||
"cURL",
|
|
||||||
"YAML",
|
|
||||||
"JSON",
|
|
||||||
]}
|
|
||||||
maximum={1}
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
<Tab title="Raw Text">
|
|
||||||
<TextArea
|
|
||||||
bind:value={$data.raw}
|
|
||||||
on:change={() => (lastTouched = "raw")}
|
|
||||||
label={"Paste raw text"}
|
|
||||||
placeholder={'e.g. curl --location --request GET "https://example.com"'}
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
</Layout>
|
|
||||||
</ModalContent>
|
|
|
@ -43,7 +43,7 @@
|
||||||
|
|
||||||
const validateDescription = description => {
|
const validateDescription = description => {
|
||||||
if (!description?.length) {
|
if (!description?.length) {
|
||||||
return "Please enter a name"
|
return "Please enter a description"
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
export const DropdownPosition = {
|
||||||
|
Relative: "top",
|
||||||
|
Absolute: "right",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Label } from "@budibase/bbui"
|
import { Label } from "@budibase/bbui"
|
||||||
import { onMount, createEventDispatcher, onDestroy } from "svelte"
|
import { onMount, createEventDispatcher, onDestroy } from "svelte"
|
||||||
|
@ -47,6 +54,7 @@
|
||||||
import { EditorModes } from "./"
|
import { EditorModes } from "./"
|
||||||
import { themeStore } from "@/stores/portal"
|
import { themeStore } from "@/stores/portal"
|
||||||
import type { EditorMode } from "@budibase/types"
|
import type { EditorMode } from "@budibase/types"
|
||||||
|
import { tooltips } from "@codemirror/view"
|
||||||
import type { BindingCompletion, CodeValidator } from "@/types"
|
import type { BindingCompletion, CodeValidator } from "@/types"
|
||||||
import { validateHbsTemplate } from "./validator/hbs"
|
import { validateHbsTemplate } from "./validator/hbs"
|
||||||
import { validateJsTemplate } from "./validator/js"
|
import { validateJsTemplate } from "./validator/js"
|
||||||
|
@ -62,11 +70,13 @@
|
||||||
export let jsBindingWrapping = true
|
export let jsBindingWrapping = true
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let readonlyLineNumbers = false
|
export let readonlyLineNumbers = false
|
||||||
|
export let dropdown = DropdownPosition.Relative
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let textarea: HTMLDivElement
|
let textarea: HTMLDivElement
|
||||||
let editor: EditorView
|
let editor: EditorView
|
||||||
|
let editorEle: HTMLDivElement
|
||||||
let mounted = false
|
let mounted = false
|
||||||
let isEditorInitialised = false
|
let isEditorInitialised = false
|
||||||
let queuedRefresh = false
|
let queuedRefresh = false
|
||||||
|
@ -117,7 +127,6 @@
|
||||||
queuedRefresh = true
|
queuedRefresh = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
editor &&
|
editor &&
|
||||||
value &&
|
value &&
|
||||||
|
@ -271,16 +280,15 @@
|
||||||
EditorView.inputHandler.of((view, from, to, insert) => {
|
EditorView.inputHandler.of((view, from, to, insert) => {
|
||||||
if (jsBindingWrapping && insert === "$") {
|
if (jsBindingWrapping && insert === "$") {
|
||||||
let { text } = view.state.doc.lineAt(from)
|
let { text } = view.state.doc.lineAt(from)
|
||||||
|
|
||||||
const left = from ? text.substring(0, from) : ""
|
const left = from ? text.substring(0, from) : ""
|
||||||
const right = to ? text.substring(to) : ""
|
const right = to ? text.substring(to) : ""
|
||||||
const wrap = !left.includes('$("') || !right.includes('")')
|
const wrap =
|
||||||
|
(!left.includes('$("') || !right.includes('")')) &&
|
||||||
|
!(left.includes("`") && right.includes("`"))
|
||||||
|
const anchor = from + (wrap ? 3 : 1)
|
||||||
const tr = view.state.update(
|
const tr = view.state.update(
|
||||||
{
|
{
|
||||||
changes: [{ from, insert: wrap ? '$("")' : "$" }],
|
changes: [{ from, insert: wrap ? '$("")' : "$" }],
|
||||||
selection: {
|
|
||||||
anchor: from + (wrap ? 3 : 1),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scrollIntoView: true,
|
scrollIntoView: true,
|
||||||
|
@ -288,6 +296,19 @@
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
view.dispatch(tr)
|
view.dispatch(tr)
|
||||||
|
// the selection needs to fired after the dispatch - this seems
|
||||||
|
// to fix an issue with the cursor not moving when the editor is
|
||||||
|
// first loaded, the first usage of the editor is not ready
|
||||||
|
// for the anchor to move as well as perform a change
|
||||||
|
setTimeout(() => {
|
||||||
|
view.dispatch(
|
||||||
|
view.state.update({
|
||||||
|
selection: {
|
||||||
|
anchor,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}, 1)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -369,14 +390,25 @@
|
||||||
const baseExtensions = buildBaseExtensions()
|
const baseExtensions = buildBaseExtensions()
|
||||||
|
|
||||||
editor = new EditorView({
|
editor = new EditorView({
|
||||||
doc: value?.toString(),
|
doc: String(value),
|
||||||
extensions: buildExtensions(baseExtensions),
|
extensions: buildExtensions([
|
||||||
|
...baseExtensions,
|
||||||
|
dropdown == DropdownPosition.Absolute
|
||||||
|
? tooltips({
|
||||||
|
position: "absolute",
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
]),
|
||||||
parent: textarea,
|
parent: textarea,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
mounted = true
|
mounted = true
|
||||||
|
// Capture scrolling
|
||||||
|
editorEle.addEventListener("wheel", e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
@ -391,7 +423,8 @@
|
||||||
<Label size="S">{label}</Label>
|
<Label size="S">{label}</Label>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class={`code-editor ${mode?.name || ""}`}>
|
|
||||||
|
<div class={`code-editor ${mode?.name || ""}`} bind:this={editorEle}>
|
||||||
<div tabindex="-1" bind:this={textarea} />
|
<div tabindex="-1" bind:this={textarea} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -400,6 +433,7 @@
|
||||||
.code-editor {
|
.code-editor {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
cursor: text;
|
||||||
}
|
}
|
||||||
.code-editor :global(.cm-editor) {
|
.code-editor :global(.cm-editor) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -559,12 +593,11 @@
|
||||||
|
|
||||||
/* Live binding value / helper container */
|
/* Live binding value / helper container */
|
||||||
.code-editor :global(.cm-completionInfo) {
|
.code-editor :global(.cm-completionInfo) {
|
||||||
margin-left: var(--spacing-s);
|
margin: 0px var(--spacing-s);
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
border-radius: var(--border-radius-s);
|
border-radius: var(--border-radius-s);
|
||||||
background-color: var(--spectrum-global-color-gray-50);
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
padding: var(--spacing-m);
|
padding: var(--spacing-m);
|
||||||
margin-top: -2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Wrapper around helpers */
|
/* Wrapper around helpers */
|
||||||
|
@ -589,6 +622,7 @@
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
max-height: 480px;
|
max-height: 480px;
|
||||||
}
|
}
|
||||||
.code-editor :global(.binding__example.helper) {
|
.code-editor :global(.binding__example.helper) {
|
||||||
|
|
|
@ -354,7 +354,7 @@
|
||||||
{#if mode === BindingMode.Text}
|
{#if mode === BindingMode.Text}
|
||||||
{#key completions}
|
{#key completions}
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={hbsValue}
|
value={hbsValue || ""}
|
||||||
on:change={onChangeHBSValue}
|
on:change={onChangeHBSValue}
|
||||||
bind:getCaretPosition
|
bind:getCaretPosition
|
||||||
bind:insertAtPos
|
bind:insertAtPos
|
||||||
|
@ -369,7 +369,7 @@
|
||||||
{:else if mode === BindingMode.JavaScript}
|
{:else if mode === BindingMode.JavaScript}
|
||||||
{#key completions}
|
{#key completions}
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={jsValue ? decodeJSBinding(jsValue) : jsValue}
|
value={jsValue ? decodeJSBinding(jsValue) : ""}
|
||||||
on:change={onChangeJSValue}
|
on:change={onChangeJSValue}
|
||||||
{completions}
|
{completions}
|
||||||
{validations}
|
{validations}
|
||||||
|
|
|
@ -0,0 +1,173 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import {
|
||||||
|
decodeJSBinding,
|
||||||
|
encodeJSBinding,
|
||||||
|
processObjectSync,
|
||||||
|
} from "@budibase/string-templates"
|
||||||
|
import { runtimeToReadableBinding } from "@/dataBinding"
|
||||||
|
import CodeEditor, { DropdownPosition } from "../CodeEditor/CodeEditor.svelte"
|
||||||
|
import {
|
||||||
|
getHelperCompletions,
|
||||||
|
jsAutocomplete,
|
||||||
|
snippetAutoComplete,
|
||||||
|
EditorModes,
|
||||||
|
bindingsToCompletions,
|
||||||
|
jsHelperAutocomplete,
|
||||||
|
} from "../CodeEditor"
|
||||||
|
import { JsonFormatter } from "@budibase/frontend-core"
|
||||||
|
import { licensing } from "@/stores/portal"
|
||||||
|
import type {
|
||||||
|
EnrichedBinding,
|
||||||
|
Snippet,
|
||||||
|
CaretPositionFn,
|
||||||
|
InsertAtPositionFn,
|
||||||
|
JSONValue,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import type { BindingCompletion, BindingCompletionOption } from "@/types"
|
||||||
|
import { snippets } from "@/stores/builder"
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
export let bindings: EnrichedBinding[] = []
|
||||||
|
export let value: string = ""
|
||||||
|
export let allowHelpers = true
|
||||||
|
export let allowSnippets = true
|
||||||
|
export let context = null
|
||||||
|
export let autofocusEditor = false
|
||||||
|
export let placeholder = null
|
||||||
|
export let height = 180
|
||||||
|
|
||||||
|
let getCaretPosition: CaretPositionFn | undefined
|
||||||
|
let insertAtPos: InsertAtPositionFn | undefined
|
||||||
|
|
||||||
|
$: readable = runtimeToReadableBinding(bindings, value || "")
|
||||||
|
$: jsValue = decodeJSBinding(readable)
|
||||||
|
|
||||||
|
$: useSnippets = allowSnippets && !$licensing.isFreePlan
|
||||||
|
$: enrichedBindings = enrichBindings(bindings, context, $snippets)
|
||||||
|
$: editorMode = EditorModes.JS
|
||||||
|
$: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode)
|
||||||
|
$: jsCompletions = getJSCompletions(bindingCompletions, $snippets, {
|
||||||
|
useHelpers: allowHelpers,
|
||||||
|
useSnippets,
|
||||||
|
})
|
||||||
|
|
||||||
|
const getJSCompletions = (
|
||||||
|
bindingCompletions: BindingCompletionOption[],
|
||||||
|
snippets: Snippet[] | null,
|
||||||
|
config: {
|
||||||
|
useHelpers: boolean
|
||||||
|
useSnippets: boolean
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const completions: BindingCompletion[] = []
|
||||||
|
if (bindingCompletions.length) {
|
||||||
|
completions.push(jsAutocomplete([...bindingCompletions]))
|
||||||
|
}
|
||||||
|
if (config.useHelpers) {
|
||||||
|
completions.push(
|
||||||
|
jsHelperAutocomplete([...getHelperCompletions(EditorModes.JS)])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (config.useSnippets && snippets) {
|
||||||
|
completions.push(snippetAutoComplete(snippets))
|
||||||
|
}
|
||||||
|
return completions
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlightJSON = (json: JSONValue) => {
|
||||||
|
return JsonFormatter.format(json, {
|
||||||
|
keyColor: "#e06c75",
|
||||||
|
numberColor: "#e5c07b",
|
||||||
|
stringColor: "#98c379",
|
||||||
|
trueColor: "#d19a66",
|
||||||
|
falseColor: "#d19a66",
|
||||||
|
nullColor: "#c678dd",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrichBindings = (
|
||||||
|
bindings: EnrichedBinding[],
|
||||||
|
context: any,
|
||||||
|
snippets: Snippet[] | null
|
||||||
|
) => {
|
||||||
|
// Create a single big array to enrich in one go
|
||||||
|
const bindingStrings = bindings.map(binding => {
|
||||||
|
if (binding.runtimeBinding.startsWith('trim "')) {
|
||||||
|
// Account for nasty hardcoded HBS bindings for roles, for legacy
|
||||||
|
// compatibility
|
||||||
|
return `{{ ${binding.runtimeBinding} }}`
|
||||||
|
} else {
|
||||||
|
return `{{ literal ${binding.runtimeBinding} }}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const bindingEvaluations = processObjectSync(bindingStrings, {
|
||||||
|
...context,
|
||||||
|
snippets,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Enrich bindings with evaluations and highlighted HTML
|
||||||
|
return bindings.map((binding, idx) => {
|
||||||
|
if (!context || typeof bindingEvaluations !== "object") {
|
||||||
|
return binding
|
||||||
|
}
|
||||||
|
const evalObj: Record<any, any> = bindingEvaluations
|
||||||
|
const value = JSON.stringify(evalObj[idx], null, 2)
|
||||||
|
return {
|
||||||
|
...binding,
|
||||||
|
value,
|
||||||
|
valueHTML: highlightJSON(value),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateValue = (val: any) => {
|
||||||
|
dispatch("change", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChangeJSValue = (e: { detail: string }) => {
|
||||||
|
if (!e.detail?.trim()) {
|
||||||
|
// Don't bother saving empty values as JS
|
||||||
|
updateValue(null)
|
||||||
|
} else {
|
||||||
|
updateValue(encodeJSBinding(e.detail))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="code-panel" style="height:{height}px;">
|
||||||
|
<div class="editor">
|
||||||
|
{#key jsCompletions}
|
||||||
|
<CodeEditor
|
||||||
|
value={jsValue || ""}
|
||||||
|
on:change={onChangeJSValue}
|
||||||
|
on:blur
|
||||||
|
completions={jsCompletions}
|
||||||
|
mode={EditorModes.JS}
|
||||||
|
bind:getCaretPosition
|
||||||
|
bind:insertAtPos
|
||||||
|
autofocus={autofocusEditor}
|
||||||
|
placeholder={placeholder ||
|
||||||
|
"Add bindings by typing $ or use the menu on the right"}
|
||||||
|
jsBindingWrapping
|
||||||
|
dropdown={DropdownPosition.Absolute}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.code-panel {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editor */
|
||||||
|
.editor {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,68 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import {
|
||||||
|
ClientBindingPanel,
|
||||||
|
DrawerBindableSlot,
|
||||||
|
} from "@/components/common/bindings"
|
||||||
|
import CodeEditorField from "@/components/common/bindings/CodeEditorField.svelte"
|
||||||
|
|
||||||
|
export let value = ""
|
||||||
|
export let panel = ClientBindingPanel
|
||||||
|
export let schema = null
|
||||||
|
export let bindings = []
|
||||||
|
export let context = {}
|
||||||
|
export let height = 180
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<DrawerBindableSlot
|
||||||
|
{panel}
|
||||||
|
{schema}
|
||||||
|
{value}
|
||||||
|
{bindings}
|
||||||
|
{context}
|
||||||
|
title="Edit Code"
|
||||||
|
type="longform"
|
||||||
|
allowJS={true}
|
||||||
|
allowHBS={false}
|
||||||
|
updateOnChange={false}
|
||||||
|
on:change={e => {
|
||||||
|
value = e.detail
|
||||||
|
dispatch("change", value)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="code-editor-wrapper">
|
||||||
|
<CodeEditorField
|
||||||
|
{value}
|
||||||
|
{bindings}
|
||||||
|
{context}
|
||||||
|
{height}
|
||||||
|
allowHBS={false}
|
||||||
|
allowJS
|
||||||
|
placeholder={"Add bindings by typing $"}
|
||||||
|
on:change={e => (value = e.detail)}
|
||||||
|
on:blur={() => dispatch("change", value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DrawerBindableSlot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper :global(.icon.slot-icon) {
|
||||||
|
top: 1px;
|
||||||
|
border-radius: 0 4px 0 4px;
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
}
|
||||||
|
.wrapper :global(.cm-editor),
|
||||||
|
.wrapper :global(.cm-scroller) {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.code-editor-wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-400);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -22,6 +22,8 @@
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let type
|
export let type
|
||||||
export let schema
|
export let schema
|
||||||
|
export let allowHBS = true
|
||||||
|
export let context = {}
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
|
@ -147,7 +149,7 @@
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div class="control" class:disabled>
|
<div class="control" class:disabled>
|
||||||
{#if !isValid(value)}
|
{#if !isValid(value) && !$$slots.default}
|
||||||
<Input
|
<Input
|
||||||
{label}
|
{label}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
@ -171,7 +173,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<slot />
|
<slot />
|
||||||
{/if}
|
{/if}
|
||||||
{#if !disabled && type !== "formula" && !disabled && !attachmentTypes.includes(type)}
|
{#if !disabled && type !== "formula" && !attachmentTypes.includes(type)}
|
||||||
<div
|
<div
|
||||||
class={`icon ${getIconClass(value, type)}`}
|
class={`icon ${getIconClass(value, type)}`}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -187,7 +189,6 @@
|
||||||
on:drawerShow
|
on:drawerShow
|
||||||
bind:this={bindingDrawer}
|
bind:this={bindingDrawer}
|
||||||
title={title ?? placeholder ?? "Bindings"}
|
title={title ?? placeholder ?? "Bindings"}
|
||||||
forceModal={true}
|
|
||||||
>
|
>
|
||||||
<Button cta slot="buttons" on:click={saveBinding}>Save</Button>
|
<Button cta slot="buttons" on:click={saveBinding}>Save</Button>
|
||||||
<svelte:component
|
<svelte:component
|
||||||
|
@ -197,7 +198,9 @@
|
||||||
on:change={event => (tempValue = event.detail)}
|
on:change={event => (tempValue = event.detail)}
|
||||||
{bindings}
|
{bindings}
|
||||||
{allowJS}
|
{allowJS}
|
||||||
|
{allowHBS}
|
||||||
{allowHelpers}
|
{allowHelpers}
|
||||||
|
{context}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
@ -208,22 +211,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.slot-icon {
|
.slot-icon {
|
||||||
right: 31px !important;
|
right: 31px;
|
||||||
border-right: 1px solid var(--spectrum-alias-border-color);
|
border-right: 1px solid var(--spectrum-alias-border-color);
|
||||||
border-top-right-radius: 0px !important;
|
border-top-right-radius: 0px;
|
||||||
border-bottom-right-radius: 0px !important;
|
border-bottom-right-radius: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-area-slot-icon {
|
.text-area-slot-icon {
|
||||||
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
||||||
border-bottom-right-radius: 0px !important;
|
border-bottom-right-radius: 0px;
|
||||||
top: 1px !important;
|
top: 1px;
|
||||||
}
|
}
|
||||||
.json-slot-icon {
|
.json-slot-icon {
|
||||||
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
||||||
border-bottom-right-radius: 0px !important;
|
border-bottom-right-radius: 0px;
|
||||||
top: 1px !important;
|
top: 1px;
|
||||||
right: 0px !important;
|
right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let allowJS = false
|
export let allowJS = false
|
||||||
|
export let allowHBS = true
|
||||||
export let context = null
|
export let context = null
|
||||||
|
|
||||||
$: enrichedBindings = enrichBindings(bindings)
|
$: enrichedBindings = enrichBindings(bindings)
|
||||||
|
@ -22,8 +23,10 @@
|
||||||
<BindingPanel
|
<BindingPanel
|
||||||
bindings={enrichedBindings}
|
bindings={enrichedBindings}
|
||||||
snippets={$snippets}
|
snippets={$snippets}
|
||||||
|
allowHelpers
|
||||||
{value}
|
{value}
|
||||||
{allowJS}
|
{allowJS}
|
||||||
|
{allowHBS}
|
||||||
{context}
|
{context}
|
||||||
on:change
|
on:change
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { Label, Select, Body } from "@budibase/bbui"
|
import { Label, Select, Body } from "@budibase/bbui"
|
||||||
import { findAllMatchingComponents } from "@/helpers/components"
|
import { findAllMatchingComponents } from "@/helpers/components"
|
||||||
import { selectedScreen } from "@/stores/builder"
|
import { selectedScreen } from "@/stores/builder"
|
||||||
|
import { InlineAlert } from "@budibase/bbui"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
|
||||||
|
@ -27,6 +28,12 @@
|
||||||
<Label small>Table</Label>
|
<Label small>Table</Label>
|
||||||
<Select bind:value={parameters.componentId} options={componentOptions} />
|
<Select bind:value={parameters.componentId} options={componentOptions} />
|
||||||
</div>
|
</div>
|
||||||
|
<InlineAlert
|
||||||
|
header="Legacy action"
|
||||||
|
message="This action is only compatible with the (deprecated) Table Block. Please see the documentation for further info."
|
||||||
|
link="https://docs.budibase.com/docs/data-actions#clear-row-selection"
|
||||||
|
linkText="Budibase Documentation"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script>
|
||||||
|
import { Label, Checkbox } from "@budibase/bbui"
|
||||||
|
import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
|
export let parameters
|
||||||
|
export let bindings = []
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<Label>Text to copy</Label>
|
||||||
|
<DrawerBindableInput
|
||||||
|
title="Text to copy"
|
||||||
|
{bindings}
|
||||||
|
value={parameters.textToCopy}
|
||||||
|
on:change={e => (parameters.textToCopy = e.detail)}
|
||||||
|
/>
|
||||||
|
<Label />
|
||||||
|
<Checkbox text="Show notification" bind:value={parameters.showNotification} />
|
||||||
|
{#if parameters.showNotification}
|
||||||
|
<Label>Notification message</Label>
|
||||||
|
<DrawerBindableInput
|
||||||
|
title="Notification message"
|
||||||
|
{bindings}
|
||||||
|
value={parameters.notificationMessage}
|
||||||
|
placeholder="Copied to clipboard"
|
||||||
|
on:change={e => (parameters.notificationMessage = e.detail)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
display: grid;
|
||||||
|
column-gap: var(--spacing-l);
|
||||||
|
row-gap: var(--spacing-s);
|
||||||
|
grid-template-columns: 120px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -26,3 +26,4 @@ export { default as CloseModal } from "./CloseModal.svelte"
|
||||||
export { default as ClearRowSelection } from "./ClearRowSelection.svelte"
|
export { default as ClearRowSelection } from "./ClearRowSelection.svelte"
|
||||||
export { default as DownloadFile } from "./DownloadFile.svelte"
|
export { default as DownloadFile } from "./DownloadFile.svelte"
|
||||||
export { default as RowAction } from "./RowAction.svelte"
|
export { default as RowAction } from "./RowAction.svelte"
|
||||||
|
export { default as CopyToClipboard } from "./CopyToClipboard.svelte"
|
||||||
|
|
|
@ -183,6 +183,17 @@
|
||||||
"name": "Row Action",
|
"name": "Row Action",
|
||||||
"type": "data",
|
"type": "data",
|
||||||
"component": "RowAction"
|
"component": "RowAction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Copy To Clipboard",
|
||||||
|
"type": "data",
|
||||||
|
"component": "CopyToClipboard",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Copied text",
|
||||||
|
"value": "copied"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
export let datasource
|
export let datasource
|
||||||
export let builderType
|
export let builderType
|
||||||
export let docsURL
|
export let docsURL
|
||||||
|
export let evaluationContext = {}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CoreFilterBuilder
|
<CoreFilterBuilder
|
||||||
|
@ -32,5 +33,6 @@
|
||||||
{allowOnEmpty}
|
{allowOnEmpty}
|
||||||
{builderType}
|
{builderType}
|
||||||
{docsURL}
|
{docsURL}
|
||||||
|
{evaluationContext}
|
||||||
on:change
|
on:change
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
export let allowJS = false
|
export let allowJS = false
|
||||||
export let actionButtonDisabled = false
|
export let actionButtonDisabled = false
|
||||||
export let compare = (option, value) => option === value
|
export let compare = (option, value) => option === value
|
||||||
|
export let context = null
|
||||||
|
|
||||||
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
||||||
name,
|
name,
|
||||||
|
@ -132,6 +133,7 @@
|
||||||
{allowJS}
|
{allowJS}
|
||||||
{allowHelpers}
|
{allowHelpers}
|
||||||
drawerLeft={bindingDrawerLeft}
|
drawerLeft={bindingDrawerLeft}
|
||||||
|
{context}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Input readonly={readOnly} bind:value={field.name} on:blur={changed} />
|
<Input readonly={readOnly} bind:value={field.name} on:blur={changed} />
|
||||||
|
@ -158,6 +160,7 @@
|
||||||
{allowJS}
|
{allowJS}
|
||||||
{allowHelpers}
|
{allowHelpers}
|
||||||
drawerLeft={bindingDrawerLeft}
|
drawerLeft={bindingDrawerLeft}
|
||||||
|
{context}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Input
|
<Input
|
||||||
|
|
|
@ -29,7 +29,12 @@
|
||||||
let modal
|
let modal
|
||||||
let webhookModal
|
let webhookModal
|
||||||
|
|
||||||
onMount(() => {
|
onMount(async () => {
|
||||||
|
await automationStore.actions.initAppSelf()
|
||||||
|
|
||||||
|
// Init the binding evaluation context
|
||||||
|
automationStore.actions.initContext()
|
||||||
|
|
||||||
$automationStore.showTestPanel = false
|
$automationStore.showTestPanel = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import {
|
import {
|
||||||
keepOpen,
|
keepOpen,
|
||||||
|
@ -14,13 +14,14 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { datasources, queries } from "@/stores/builder"
|
import { datasources, queries } from "@/stores/builder"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
import type { Datasource } from "@budibase/types"
|
||||||
|
|
||||||
export let navigateDatasource = false
|
export let navigateDatasource = false
|
||||||
export let datasourceId
|
export let datasourceId: string | undefined = undefined
|
||||||
export let createDatasource = false
|
export let createDatasource = false
|
||||||
export let onCancel
|
export let onCancel: (() => void) | undefined = undefined
|
||||||
|
|
||||||
const data = writable({
|
const data = writable<{ url: string; raw: string; file?: any }>({
|
||||||
url: "",
|
url: "",
|
||||||
raw: "",
|
raw: "",
|
||||||
file: undefined,
|
file: undefined,
|
||||||
|
@ -28,12 +29,14 @@
|
||||||
|
|
||||||
let lastTouched = "url"
|
let lastTouched = "url"
|
||||||
|
|
||||||
const getData = async () => {
|
$: datasource = $datasources.selected as Datasource
|
||||||
|
|
||||||
|
const getData = async (): Promise<string> => {
|
||||||
let dataString
|
let dataString
|
||||||
|
|
||||||
// parse the file into memory and send as string
|
// parse the file into memory and send as string
|
||||||
if (lastTouched === "file") {
|
if (lastTouched === "file") {
|
||||||
dataString = await $data.file.text()
|
dataString = await $data.file?.text()
|
||||||
} else if (lastTouched === "url") {
|
} else if (lastTouched === "url") {
|
||||||
const response = await fetch($data.url)
|
const response = await fetch($data.url)
|
||||||
dataString = await response.text()
|
dataString = await response.text()
|
||||||
|
@ -55,9 +58,9 @@
|
||||||
const body = {
|
const body = {
|
||||||
data: dataString,
|
data: dataString,
|
||||||
datasourceId,
|
datasourceId,
|
||||||
|
datasource,
|
||||||
}
|
}
|
||||||
|
const importResult = await queries.importQueries(body)
|
||||||
const importResult = await queries.import(body)
|
|
||||||
if (!datasourceId) {
|
if (!datasourceId) {
|
||||||
datasourceId = importResult.datasourceId
|
datasourceId = importResult.datasourceId
|
||||||
}
|
}
|
||||||
|
@ -71,8 +74,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.success("Imported successfully")
|
notifications.success("Imported successfully")
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
notifications.error("Error importing queries")
|
notifications.error(`Error importing queries - ${error.message}`)
|
||||||
|
|
||||||
return keepOpen
|
return keepOpen
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { derived, get } from "svelte/store"
|
import { derived, get, readable, Readable } from "svelte/store"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { createHistoryStore, HistoryStore } from "@/stores/builder/history"
|
import { createHistoryStore, HistoryStore } from "@/stores/builder/history"
|
||||||
import { licensing } from "@/stores/portal"
|
import { licensing, organisation, environment } from "@/stores/portal"
|
||||||
import { tables, appStore } from "@/stores/builder"
|
import { tables, appStore } from "@/stores/builder"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
|
@ -35,9 +35,18 @@ import {
|
||||||
BranchStep,
|
BranchStep,
|
||||||
GetAutomationTriggerDefinitionsResponse,
|
GetAutomationTriggerDefinitionsResponse,
|
||||||
GetAutomationActionDefinitionsResponse,
|
GetAutomationActionDefinitionsResponse,
|
||||||
|
AppSelfResponse,
|
||||||
|
TestAutomationResponse,
|
||||||
|
isAutomationResults,
|
||||||
|
RowActionTriggerOutputs,
|
||||||
|
WebhookTriggerOutputs,
|
||||||
|
AutomationCustomIOType,
|
||||||
|
AutomationTriggerResultOutputs,
|
||||||
|
AutomationTriggerResult,
|
||||||
|
AutomationStepType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { ActionStepID } from "@/constants/backend/automations"
|
import { ActionStepID, TriggerStepID } from "@/constants/backend/automations"
|
||||||
import { FIELDS } from "@/constants/backend"
|
import { FIELDS as COLUMNS } from "@/constants/backend"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
import { rowActions } from "./rowActions"
|
import { rowActions } from "./rowActions"
|
||||||
import { getNewStepName } from "@/helpers/automations/nameHelpers"
|
import { getNewStepName } from "@/helpers/automations/nameHelpers"
|
||||||
|
@ -46,10 +55,11 @@ import { BudiStore, DerivedBudiStore } from "@/stores/BudiStore"
|
||||||
|
|
||||||
interface AutomationState {
|
interface AutomationState {
|
||||||
automations: Automation[]
|
automations: Automation[]
|
||||||
testResults: any | null
|
testResults?: TestAutomationResponse
|
||||||
showTestPanel: boolean
|
showTestPanel: boolean
|
||||||
blockDefinitions: BlockDefinitions
|
blockDefinitions: BlockDefinitions
|
||||||
selectedAutomationId: string | null
|
selectedAutomationId: string | null
|
||||||
|
appSelf?: AppSelfResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DerivedAutomationState extends AutomationState {
|
interface DerivedAutomationState extends AutomationState {
|
||||||
|
@ -59,7 +69,6 @@ interface DerivedAutomationState extends AutomationState {
|
||||||
|
|
||||||
const initialAutomationState: AutomationState = {
|
const initialAutomationState: AutomationState = {
|
||||||
automations: [],
|
automations: [],
|
||||||
testResults: null,
|
|
||||||
showTestPanel: false,
|
showTestPanel: false,
|
||||||
blockDefinitions: {
|
blockDefinitions: {
|
||||||
TRIGGER: {},
|
TRIGGER: {},
|
||||||
|
@ -91,6 +100,116 @@ const getFinalDefinitions = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const automationActions = (store: AutomationStore) => ({
|
const automationActions = (store: AutomationStore) => ({
|
||||||
|
/**
|
||||||
|
* Generates a derived store acting as an evaluation context
|
||||||
|
* for bindings in automations
|
||||||
|
*
|
||||||
|
* @returns {Readable<AutomationContext>}
|
||||||
|
*/
|
||||||
|
generateContext: (): Readable<AutomationContext> => {
|
||||||
|
return derived(
|
||||||
|
[organisation, store.selected, environment, tables],
|
||||||
|
([$organisation, $selectedAutomation, $env, $tables]) => {
|
||||||
|
const { platformUrl: url, company, logoUrl: logo } = $organisation
|
||||||
|
|
||||||
|
const results: TestAutomationResponse | undefined =
|
||||||
|
$selectedAutomation?.testResults
|
||||||
|
|
||||||
|
const testData: AutomationTriggerResultOutputs | undefined =
|
||||||
|
$selectedAutomation.data?.testData
|
||||||
|
const triggerDef = $selectedAutomation.data?.definition?.trigger
|
||||||
|
|
||||||
|
const isWebhook = triggerDef?.stepId === TriggerStepID.WEBHOOK
|
||||||
|
const isRowAction = triggerDef?.stepId === TriggerStepID.ROW_ACTION
|
||||||
|
const rowActionTableId = triggerDef?.inputs?.tableId
|
||||||
|
const rowActionTable = rowActionTableId
|
||||||
|
? $tables.list.find(table => table._id === rowActionTableId)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
let triggerData: AutomationTriggerResultOutputs | undefined
|
||||||
|
|
||||||
|
if (results && isAutomationResults(results)) {
|
||||||
|
const automationTrigger: AutomationTriggerResult | undefined =
|
||||||
|
results?.trigger
|
||||||
|
|
||||||
|
const outputs: AutomationTriggerResultOutputs | undefined =
|
||||||
|
automationTrigger?.outputs
|
||||||
|
triggerData = outputs ? outputs : undefined
|
||||||
|
|
||||||
|
if (triggerData) {
|
||||||
|
if (isRowAction && rowActionTable) {
|
||||||
|
const rowTrigger = triggerData as RowActionTriggerOutputs
|
||||||
|
// Row action table must always be retrieved as it is never
|
||||||
|
// returned in the test results
|
||||||
|
rowTrigger.table = rowActionTable
|
||||||
|
} else if (isWebhook) {
|
||||||
|
const webhookTrigger = triggerData as WebhookTriggerOutputs
|
||||||
|
// Ensure it displays in the event that the configuration have been skipped
|
||||||
|
webhookTrigger.body = webhookTrigger.body ?? {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up unnecessary data from the context
|
||||||
|
// Meta contains UI/UX config data. Non-bindable
|
||||||
|
delete triggerData?.meta
|
||||||
|
} else {
|
||||||
|
// Substitute test data in place of the trigger data if the test hasn't been run
|
||||||
|
triggerData = testData
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppSelf context required to mirror server user context
|
||||||
|
const userContext = $selectedAutomation.appSelf || {}
|
||||||
|
|
||||||
|
// Extract step results from a valid response
|
||||||
|
const stepResults =
|
||||||
|
results && isAutomationResults(results) ? results?.steps : []
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: userContext,
|
||||||
|
// Merge in the trigger data.
|
||||||
|
...(triggerData ? { trigger: { ...triggerData } } : {}),
|
||||||
|
// This will initially be empty for each step but will populate
|
||||||
|
// upon running the test.
|
||||||
|
steps: stepResults.reduce(
|
||||||
|
(acc: Record<string, any>, res: Record<string, any>) => {
|
||||||
|
acc[res.id] = res.outputs
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
env: ($env?.variables || []).reduce(
|
||||||
|
(acc: Record<string, any>, variable: Record<string, any>) => {
|
||||||
|
acc[variable.name] = ""
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
settings: { url, company, logo },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise the automation evaluation context
|
||||||
|
*/
|
||||||
|
initContext: () => {
|
||||||
|
store.context = store.actions.generateContext()
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Fetches the app user context used for live evaluation
|
||||||
|
* This matches the context used on the server
|
||||||
|
* @returns {AppSelfResponse | null}
|
||||||
|
*/
|
||||||
|
initAppSelf: async (): Promise<AppSelfResponse | null> => {
|
||||||
|
// Fetch and update the app self if it hasn't been set
|
||||||
|
const appSelfResponse = await API.fetchSelf()
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
...(appSelfResponse ? { appSelf: appSelfResponse } : {}),
|
||||||
|
}))
|
||||||
|
return appSelfResponse
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Move a given block from one location on the tree to another.
|
* Move a given block from one location on the tree to another.
|
||||||
*
|
*
|
||||||
|
@ -287,9 +406,12 @@ const automationActions = (store: AutomationStore) => ({
|
||||||
* Build a sequential list of all steps on the step path provided
|
* Build a sequential list of all steps on the step path provided
|
||||||
*
|
*
|
||||||
* @param {Array<Object>} pathWay e.g. [{stepIdx:2},{branchIdx:0, stepIdx:2},...]
|
* @param {Array<Object>} pathWay e.g. [{stepIdx:2},{branchIdx:0, stepIdx:2},...]
|
||||||
* @returns {Array<Object>} all steps encountered on the provided path
|
* @returns {Array<AutomationStep | AutomationTrigger>} all steps encountered on the provided path
|
||||||
*/
|
*/
|
||||||
getPathSteps: (pathWay: Array<BranchPath>, automation: Automation) => {
|
getPathSteps: (
|
||||||
|
pathWay: Array<BranchPath>,
|
||||||
|
automation: Automation
|
||||||
|
): Array<AutomationStep | AutomationTrigger> => {
|
||||||
// Base Steps, including trigger
|
// Base Steps, including trigger
|
||||||
const steps = [
|
const steps = [
|
||||||
automation.definition.trigger,
|
automation.definition.trigger,
|
||||||
|
@ -536,41 +658,72 @@ const automationActions = (store: AutomationStore) => ({
|
||||||
let bindings: any[] = []
|
let bindings: any[] = []
|
||||||
const addBinding = (
|
const addBinding = (
|
||||||
name: string,
|
name: string,
|
||||||
value: any,
|
schema: any,
|
||||||
icon: string,
|
icon: string,
|
||||||
idx: number,
|
idx: number,
|
||||||
isLoopBlock: boolean,
|
isLoopBlock: boolean,
|
||||||
bindingName?: string
|
pathBlock: AutomationStep | AutomationTrigger,
|
||||||
|
bindingName: string
|
||||||
) => {
|
) => {
|
||||||
if (!name) return
|
if (!name) return
|
||||||
|
|
||||||
const runtimeBinding = store.actions.determineRuntimeBinding(
|
const runtimeBinding = store.actions.determineRuntimeBinding(
|
||||||
name,
|
name,
|
||||||
idx,
|
idx,
|
||||||
isLoopBlock,
|
isLoopBlock,
|
||||||
bindingName,
|
|
||||||
automation,
|
automation,
|
||||||
currentBlock,
|
currentBlock,
|
||||||
pathSteps
|
pathSteps
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Skip binding if its invalid
|
||||||
|
if (!runtimeBinding) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const readableBinding = store.actions.determineReadableBinding(
|
||||||
|
name,
|
||||||
|
pathBlock
|
||||||
|
)
|
||||||
|
|
||||||
const categoryName = store.actions.determineCategoryName(
|
const categoryName = store.actions.determineCategoryName(
|
||||||
idx,
|
idx,
|
||||||
isLoopBlock,
|
isLoopBlock,
|
||||||
bindingName,
|
bindingName,
|
||||||
loopBlockCount
|
loopBlockCount
|
||||||
)
|
)
|
||||||
bindings.push(
|
|
||||||
store.actions.createBindingObject(
|
const isStep = !isLoopBlock && idx !== 0
|
||||||
name,
|
const defaultReadable =
|
||||||
value,
|
bindingName && isStep ? `steps.${bindingName}.${name}` : runtimeBinding
|
||||||
icon,
|
|
||||||
idx,
|
// Check if the schema matches any column types.
|
||||||
loopBlockCount,
|
const column = Object.values(COLUMNS).find(
|
||||||
isLoopBlock,
|
col =>
|
||||||
|
col.type === schema.type &&
|
||||||
|
("subtype" in col ? col.subtype === schema.subtype : true)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Automation types and column types can collide e.g. "array"
|
||||||
|
// Exclude where necessary
|
||||||
|
const ignoreColumnType = schema.customType === AutomationCustomIOType.ROWS
|
||||||
|
|
||||||
|
// Shown in the bindable menus
|
||||||
|
const displayType = ignoreColumnType ? schema.type : column?.name
|
||||||
|
|
||||||
|
bindings.push({
|
||||||
|
readableBinding: readableBinding || defaultReadable,
|
||||||
runtimeBinding,
|
runtimeBinding,
|
||||||
categoryName,
|
type: schema.type,
|
||||||
bindingName
|
description: schema.description,
|
||||||
)
|
icon,
|
||||||
)
|
category: categoryName,
|
||||||
|
display: {
|
||||||
|
type: displayType,
|
||||||
|
name,
|
||||||
|
rank: isLoopBlock ? idx + 1 : idx - loopBlockCount,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let loopBlockCount = 0
|
let loopBlockCount = 0
|
||||||
|
@ -638,8 +791,17 @@ const automationActions = (store: AutomationStore) => ({
|
||||||
console.error("Loop block missing.")
|
console.error("Loop block missing.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.entries(schema).forEach(([name, value]) => {
|
Object.entries(schema).forEach(([name, value]) => {
|
||||||
addBinding(name, value, icon, blockIdx, isLoopBlock, bindingName)
|
addBinding(
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
icon,
|
||||||
|
blockIdx,
|
||||||
|
isLoopBlock,
|
||||||
|
pathBlock,
|
||||||
|
bindingName
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -650,23 +812,60 @@ const automationActions = (store: AutomationStore) => ({
|
||||||
return bindings
|
return bindings
|
||||||
},
|
},
|
||||||
|
|
||||||
|
determineReadableBinding: (
|
||||||
|
name: string,
|
||||||
|
block: AutomationStep | AutomationTrigger
|
||||||
|
) => {
|
||||||
|
const rowTriggers = [
|
||||||
|
TriggerStepID.ROW_UPDATED,
|
||||||
|
TriggerStepID.ROW_SAVED,
|
||||||
|
TriggerStepID.ROW_DELETED,
|
||||||
|
TriggerStepID.ROW_ACTION,
|
||||||
|
]
|
||||||
|
|
||||||
|
const isTrigger = block.type === AutomationStepType.TRIGGER
|
||||||
|
const isAppTrigger = block.stepId === AutomationTriggerStepId.APP
|
||||||
|
const isRowTrigger = rowTriggers.includes(block.stepId)
|
||||||
|
|
||||||
|
let readableBinding = ""
|
||||||
|
if (isTrigger) {
|
||||||
|
if (isAppTrigger) {
|
||||||
|
readableBinding = `trigger.fields.${name}`
|
||||||
|
} else if (isRowTrigger) {
|
||||||
|
let noRowKeywordBindings = ["id", "revision", "oldRow"]
|
||||||
|
readableBinding = noRowKeywordBindings.includes(name)
|
||||||
|
? `trigger.${name}`
|
||||||
|
: `trigger.row.${name}`
|
||||||
|
} else {
|
||||||
|
readableBinding = `trigger.${name}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return readableBinding
|
||||||
|
},
|
||||||
|
|
||||||
determineRuntimeBinding: (
|
determineRuntimeBinding: (
|
||||||
name: string,
|
name: string,
|
||||||
idx: number,
|
idx: number,
|
||||||
isLoopBlock: boolean,
|
isLoopBlock: boolean,
|
||||||
bindingName: string | undefined,
|
|
||||||
automation: Automation,
|
automation: Automation,
|
||||||
currentBlock: AutomationStep | AutomationTrigger | undefined,
|
currentBlock: AutomationStep | AutomationTrigger | undefined,
|
||||||
pathSteps: (AutomationStep | AutomationTrigger)[]
|
pathSteps: (AutomationStep | AutomationTrigger)[]
|
||||||
) => {
|
) => {
|
||||||
let runtimeName: string | null
|
let runtimeName: string
|
||||||
|
|
||||||
|
// Legacy support for EXECUTE_SCRIPT steps
|
||||||
|
const isJSScript =
|
||||||
|
currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT
|
||||||
|
|
||||||
/* Begin special cases for generating custom schemas based on triggers */
|
/* Begin special cases for generating custom schemas based on triggers */
|
||||||
if (
|
if (
|
||||||
idx === 0 &&
|
idx === 0 &&
|
||||||
automation.definition.trigger?.event === AutomationEventType.APP_TRIGGER
|
automation.definition.trigger?.event === AutomationEventType.APP_TRIGGER
|
||||||
) {
|
) {
|
||||||
return `trigger.fields.${name}`
|
return isJSScript
|
||||||
|
? `trigger.fields["${name}"]`
|
||||||
|
: `trigger.fields.[${name}]`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -676,29 +875,28 @@ const automationActions = (store: AutomationStore) => ({
|
||||||
automation.definition.trigger?.event === AutomationEventType.ROW_SAVE)
|
automation.definition.trigger?.event === AutomationEventType.ROW_SAVE)
|
||||||
) {
|
) {
|
||||||
let noRowKeywordBindings = ["id", "revision", "oldRow"]
|
let noRowKeywordBindings = ["id", "revision", "oldRow"]
|
||||||
if (!noRowKeywordBindings.includes(name)) return `trigger.row.${name}`
|
if (!noRowKeywordBindings.includes(name)) {
|
||||||
|
return isJSScript ? `trigger.row["${name}"]` : `trigger.row.[${name}]`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* End special cases for generating custom schemas based on triggers */
|
/* End special cases for generating custom schemas based on triggers */
|
||||||
|
|
||||||
if (isLoopBlock) {
|
if (isLoopBlock) {
|
||||||
runtimeName = `loop.${name}`
|
runtimeName = `loop.${name}`
|
||||||
} else if (idx === 0) {
|
} else if (idx === 0) {
|
||||||
runtimeName = `trigger.${name}`
|
runtimeName = `trigger.[${name}]`
|
||||||
} else if (
|
} else if (isJSScript) {
|
||||||
currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT ||
|
|
||||||
currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT_V2
|
|
||||||
) {
|
|
||||||
const stepId = pathSteps[idx].id
|
const stepId = pathSteps[idx].id
|
||||||
if (!stepId) {
|
if (!stepId) {
|
||||||
notifications.error("Error generating binding: Step ID not found.")
|
notifications.error("Error generating binding: Step ID not found.")
|
||||||
return null
|
return
|
||||||
}
|
}
|
||||||
runtimeName = `steps["${stepId}"].${name}`
|
runtimeName = `steps["${stepId}"].${name}`
|
||||||
} else {
|
} else {
|
||||||
const stepId = pathSteps[idx].id
|
const stepId = pathSteps[idx].id
|
||||||
if (!stepId) {
|
if (!stepId) {
|
||||||
notifications.error("Error generating binding: Step ID not found.")
|
notifications.error("Error generating binding: Step ID not found.")
|
||||||
return null
|
return
|
||||||
}
|
}
|
||||||
runtimeName = `steps.${stepId}.${name}`
|
runtimeName = `steps.${stepId}.${name}`
|
||||||
}
|
}
|
||||||
|
@ -719,40 +917,6 @@ const automationActions = (store: AutomationStore) => ({
|
||||||
: `Step ${idx - loopBlockCount} outputs`
|
: `Step ${idx - loopBlockCount} outputs`
|
||||||
},
|
},
|
||||||
|
|
||||||
createBindingObject: (
|
|
||||||
name: string,
|
|
||||||
value: any,
|
|
||||||
icon: string,
|
|
||||||
idx: number,
|
|
||||||
loopBlockCount: number,
|
|
||||||
isLoopBlock: boolean,
|
|
||||||
runtimeBinding: string | null,
|
|
||||||
categoryName: string,
|
|
||||||
bindingName?: string
|
|
||||||
) => {
|
|
||||||
const field = Object.values(FIELDS).find(
|
|
||||||
field =>
|
|
||||||
field.type === value.type &&
|
|
||||||
("subtype" in field ? field.subtype === value.subtype : true)
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
readableBinding:
|
|
||||||
bindingName && !isLoopBlock && idx !== 0
|
|
||||||
? `steps.${bindingName}.${name}`
|
|
||||||
: runtimeBinding,
|
|
||||||
runtimeBinding,
|
|
||||||
type: value.type,
|
|
||||||
description: value.description,
|
|
||||||
icon,
|
|
||||||
category: categoryName,
|
|
||||||
display: {
|
|
||||||
type: field?.name || value.type,
|
|
||||||
name,
|
|
||||||
rank: isLoopBlock ? idx + 1 : idx - loopBlockCount,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
processBlockInputs: async (
|
processBlockInputs: async (
|
||||||
block: AutomationStep,
|
block: AutomationStep,
|
||||||
data: Record<string, any>
|
data: Record<string, any>
|
||||||
|
@ -804,19 +968,14 @@ const automationActions = (store: AutomationStore) => ({
|
||||||
},
|
},
|
||||||
|
|
||||||
test: async (automation: Automation, testData: any) => {
|
test: async (automation: Automation, testData: any) => {
|
||||||
let result: any
|
let result: TestAutomationResponse
|
||||||
try {
|
try {
|
||||||
result = await API.testAutomation(automation._id!, testData)
|
result = await API.testAutomation(automation._id!, testData)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const message = err.message || err.status || JSON.stringify(err)
|
const message = err.message || err.status || JSON.stringify(err)
|
||||||
throw `Automation test failed - ${message}`
|
throw `Automation test failed - ${message}`
|
||||||
}
|
}
|
||||||
if (!result?.trigger && !result?.steps?.length && !result?.message) {
|
|
||||||
if (result?.err?.code === "usage_limit_exceeded") {
|
|
||||||
throw "You have exceeded your automation quota"
|
|
||||||
}
|
|
||||||
throw "Something went wrong testing your automation"
|
|
||||||
}
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.testResults = result
|
state.testResults = result
|
||||||
return state
|
return state
|
||||||
|
@ -1404,7 +1563,7 @@ const automationActions = (store: AutomationStore) => ({
|
||||||
}
|
}
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.selectedAutomationId = id
|
state.selectedAutomationId = id
|
||||||
state.testResults = null
|
delete state.testResults
|
||||||
state.showTestPanel = false
|
state.showTestPanel = false
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
@ -1444,28 +1603,13 @@ const automationActions = (store: AutomationStore) => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
class AutomationStore extends BudiStore<AutomationState> {
|
export interface AutomationContext {
|
||||||
history: HistoryStore<Automation>
|
user: AppSelfResponse | null
|
||||||
actions: ReturnType<typeof automationActions>
|
trigger?: AutomationTriggerResultOutputs
|
||||||
|
steps: Record<string, AutomationStep>
|
||||||
constructor() {
|
env: Record<string, any>
|
||||||
super(initialAutomationState)
|
settings: Record<string, any>
|
||||||
this.actions = automationActions(this)
|
|
||||||
this.history = createHistoryStore({
|
|
||||||
getDoc: this.actions.getDefinition.bind(this),
|
|
||||||
selectDoc: this.actions.select.bind(this),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Then wrap save and delete with history
|
|
||||||
const originalSave = this.actions.save.bind(this.actions)
|
|
||||||
const originalDelete = this.actions.delete.bind(this.actions)
|
|
||||||
this.actions.save = this.history.wrapSaveDoc(originalSave)
|
|
||||||
this.actions.delete = this.history.wrapDeleteDoc(originalDelete)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export const automationStore = new AutomationStore()
|
|
||||||
export const automationHistoryStore = automationStore.history
|
|
||||||
|
|
||||||
export class SelectedAutomationStore extends DerivedBudiStore<
|
export class SelectedAutomationStore extends DerivedBudiStore<
|
||||||
AutomationState,
|
AutomationState,
|
||||||
|
@ -1527,4 +1671,49 @@ export class SelectedAutomationStore extends DerivedBudiStore<
|
||||||
super(initialAutomationState, makeDerivedStore)
|
super(initialAutomationState, makeDerivedStore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const selectedAutomation = new SelectedAutomationStore(automationStore)
|
|
||||||
|
class AutomationStore extends BudiStore<AutomationState> {
|
||||||
|
history: HistoryStore<Automation>
|
||||||
|
actions: ReturnType<typeof automationActions>
|
||||||
|
selected: SelectedAutomationStore
|
||||||
|
context: Readable<AutomationContext> | undefined
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(initialAutomationState)
|
||||||
|
this.actions = automationActions(this)
|
||||||
|
this.history = createHistoryStore({
|
||||||
|
getDoc: this.actions.getDefinition.bind(this),
|
||||||
|
selectDoc: this.actions.select.bind(this),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then wrap save and delete with history
|
||||||
|
const originalSave = this.actions.save.bind(this.actions)
|
||||||
|
const originalDelete = this.actions.delete.bind(this.actions)
|
||||||
|
this.actions.save = this.history.wrapSaveDoc(originalSave)
|
||||||
|
this.actions.delete = this.history.wrapDeleteDoc(originalDelete)
|
||||||
|
|
||||||
|
this.selected = new SelectedAutomationStore(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const automationStore = new AutomationStore()
|
||||||
|
|
||||||
|
export const automationHistoryStore = automationStore.history
|
||||||
|
export const selectedAutomation = automationStore.selected
|
||||||
|
|
||||||
|
// Define an empty evaluate context at the start
|
||||||
|
const emptyContext: AutomationContext = {
|
||||||
|
user: {},
|
||||||
|
steps: {},
|
||||||
|
env: {},
|
||||||
|
settings: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page layout kicks off initialisation, subscription happens within the page
|
||||||
|
export const evaluationContext: Readable<AutomationContext> = readable(
|
||||||
|
emptyContext,
|
||||||
|
set => {
|
||||||
|
const unsubscribe = automationStore.context?.subscribe(set) ?? (() => {})
|
||||||
|
return () => unsubscribe()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
automationStore,
|
automationStore,
|
||||||
selectedAutomation,
|
selectedAutomation,
|
||||||
automationHistoryStore,
|
automationHistoryStore,
|
||||||
|
evaluationContext,
|
||||||
} from "./automations"
|
} from "./automations"
|
||||||
import { userStore, userSelectedResourceMap, isOnlyUser } from "./users"
|
import { userStore, userSelectedResourceMap, isOnlyUser } from "./users"
|
||||||
import { deploymentStore } from "./deployments"
|
import { deploymentStore } from "./deployments"
|
||||||
|
@ -72,6 +73,7 @@ export {
|
||||||
snippets,
|
snippets,
|
||||||
rowActions,
|
rowActions,
|
||||||
appPublished,
|
appPublished,
|
||||||
|
evaluationContext,
|
||||||
screenComponentsList,
|
screenComponentsList,
|
||||||
screenComponentErrors,
|
screenComponentErrors,
|
||||||
screenComponentErrorList,
|
screenComponentErrorList,
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
import FormBlockWrapper from "./FormBlockWrapper.svelte"
|
import FormBlockWrapper from "./FormBlockWrapper.svelte"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import type { TableSchema, UIDatasource } from "@budibase/types"
|
import type { TableSchema } from "@budibase/types"
|
||||||
|
|
||||||
type Field = { name: string; active: boolean }
|
type Field = { name: string; active: boolean }
|
||||||
|
|
||||||
export let actionType: string
|
export let actionType: string
|
||||||
export let dataSource: UIDatasource
|
export let dataSource: { resourceId: string }
|
||||||
export let size: string
|
export let size: string
|
||||||
export let disabled: boolean
|
export let disabled: boolean
|
||||||
export let fields: (Field | string)[]
|
export let fields: (Field | string)[]
|
||||||
|
@ -30,8 +30,8 @@
|
||||||
// Legacy
|
// Legacy
|
||||||
export let showDeleteButton: boolean
|
export let showDeleteButton: boolean
|
||||||
export let showSaveButton: boolean
|
export let showSaveButton: boolean
|
||||||
export let saveButtonLabel: boolean
|
export let saveButtonLabel: string
|
||||||
export let deleteButtonLabel: boolean
|
export let deleteButtonLabel: string
|
||||||
|
|
||||||
const { fetchDatasourceSchema, generateGoldenSample } = getContext("sdk")
|
const { fetchDatasourceSchema, generateGoldenSample } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
@ -107,7 +107,7 @@
|
||||||
return [...fields, ...defaultFields].filter(field => field.active)
|
return [...fields, ...defaultFields].filter(field => field.active)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchSchema = async (datasource: UIDatasource) => {
|
const fetchSchema = async (datasource: { resourceId: string }) => {
|
||||||
schema = (await fetchDatasourceSchema(datasource)) || {}
|
schema = (await fetchDatasourceSchema(datasource)) || {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -421,6 +421,28 @@ const showNotificationHandler = action => {
|
||||||
|
|
||||||
const promptUserHandler = () => {}
|
const promptUserHandler = () => {}
|
||||||
|
|
||||||
|
const copyToClipboardHandler = async action => {
|
||||||
|
const { textToCopy, showNotification, notificationMessage } =
|
||||||
|
action.parameters
|
||||||
|
|
||||||
|
if (!textToCopy) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(textToCopy)
|
||||||
|
if (showNotification) {
|
||||||
|
const message = notificationMessage || "Copied to clipboard"
|
||||||
|
notificationStore.actions.success(message, true, 3000)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy text: ", err)
|
||||||
|
notificationStore.actions.error("Failed to copy to clipboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
return { copied: textToCopy }
|
||||||
|
}
|
||||||
|
|
||||||
const openSidePanelHandler = action => {
|
const openSidePanelHandler = action => {
|
||||||
const { id } = action.parameters
|
const { id } = action.parameters
|
||||||
if (id) {
|
if (id) {
|
||||||
|
@ -514,6 +536,7 @@ const handlerMap = {
|
||||||
["Close Modal"]: closeModalHandler,
|
["Close Modal"]: closeModalHandler,
|
||||||
["Download File"]: downloadFileHandler,
|
["Download File"]: downloadFileHandler,
|
||||||
["Row Action"]: rowActionHandler,
|
["Row Action"]: rowActionHandler,
|
||||||
|
["Copy To Clipboard"]: copyToClipboardHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmTextMap = {
|
const confirmTextMap = {
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
export let drawerTitle
|
export let drawerTitle
|
||||||
export let toReadable
|
export let toReadable
|
||||||
export let toRuntime
|
export let toRuntime
|
||||||
|
export let evaluationContext = {}
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -66,7 +67,6 @@
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={panel}
|
this={panel}
|
||||||
slot="body"
|
slot="body"
|
||||||
|
@ -76,6 +76,7 @@
|
||||||
allowHBS
|
allowHBS
|
||||||
on:change={drawerOnChange}
|
on:change={drawerOnChange}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
context={evaluationContext}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
export let panel
|
export let panel
|
||||||
export let toReadable
|
export let toReadable
|
||||||
export let toRuntime
|
export let toRuntime
|
||||||
|
export let evaluationContext = {}
|
||||||
|
|
||||||
$: editableFilters = migrateFilters(filters)
|
$: editableFilters = migrateFilters(filters)
|
||||||
$: {
|
$: {
|
||||||
|
@ -385,6 +386,7 @@
|
||||||
{panel}
|
{panel}
|
||||||
{toReadable}
|
{toReadable}
|
||||||
{toRuntime}
|
{toRuntime}
|
||||||
|
{evaluationContext}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
const updated = {
|
const updated = {
|
||||||
...filter,
|
...filter,
|
||||||
|
@ -423,6 +425,7 @@
|
||||||
{panel}
|
{panel}
|
||||||
{toReadable}
|
{toReadable}
|
||||||
{toRuntime}
|
{toRuntime}
|
||||||
|
{evaluationContext}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
onFilterFieldUpdate(
|
onFilterFieldUpdate(
|
||||||
{ ...filter, ...e.detail },
|
{ ...filter, ...e.detail },
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
export let drawerTitle
|
export let drawerTitle
|
||||||
export let toReadable
|
export let toReadable
|
||||||
export let toRuntime
|
export let toRuntime
|
||||||
|
export let evaluationContext = {}
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const { OperatorOptions, FilterValueType } = Constants
|
const { OperatorOptions, FilterValueType } = Constants
|
||||||
|
@ -156,6 +157,7 @@
|
||||||
allowHBS
|
allowHBS
|
||||||
on:change={drawerOnChange}
|
on:change={drawerOnChange}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
context={evaluationContext}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,13 @@ import { sdk as coreSdk } from "@budibase/shared-core"
|
||||||
import { DocumentType } from "../../db/utils"
|
import { DocumentType } from "../../db/utils"
|
||||||
import { updateTestHistory } from "../../automations/utils"
|
import { updateTestHistory } from "../../automations/utils"
|
||||||
import { withTestFlag } from "../../utilities/redis"
|
import { withTestFlag } from "../../utilities/redis"
|
||||||
import { context, cache, events, db as dbCore } from "@budibase/backend-core"
|
import {
|
||||||
|
context,
|
||||||
|
cache,
|
||||||
|
events,
|
||||||
|
db as dbCore,
|
||||||
|
HTTPError,
|
||||||
|
} from "@budibase/backend-core"
|
||||||
import { automations, features } from "@budibase/pro"
|
import { automations, features } from "@budibase/pro"
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
|
@ -28,6 +34,7 @@ import {
|
||||||
TriggerAutomationResponse,
|
TriggerAutomationResponse,
|
||||||
TestAutomationRequest,
|
TestAutomationRequest,
|
||||||
TestAutomationResponse,
|
TestAutomationResponse,
|
||||||
|
Table,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { getActionDefinitions as actionDefs } from "../../automations/actions"
|
import { getActionDefinitions as actionDefs } from "../../automations/actions"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
|
@ -231,14 +238,22 @@ export async function test(
|
||||||
const { request, appId } = ctx
|
const { request, appId } = ctx
|
||||||
const { body } = request
|
const { body } = request
|
||||||
|
|
||||||
|
let table: Table | undefined
|
||||||
|
if (coreSdk.automations.isRowAction(automation) && body.row?.tableId) {
|
||||||
|
table = await sdk.tables.getTable(body.row?.tableId)
|
||||||
|
if (!table) {
|
||||||
|
throw new HTTPError("Table not found", 404)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.body = await withTestFlag(automation._id!, async () => {
|
ctx.body = await withTestFlag(automation._id!, async () => {
|
||||||
const occurredAt = new Date().getTime()
|
const occurredAt = new Date().getTime()
|
||||||
await updateTestHistory(appId, automation, { ...body, occurredAt })
|
await updateTestHistory(appId, automation, { ...body, occurredAt })
|
||||||
|
const input = prepareTestInput(body)
|
||||||
const user = sdk.users.getUserContextBindings(ctx.user)
|
const user = sdk.users.getUserContextBindings(ctx.user)
|
||||||
return await triggers.externalTrigger(
|
return await triggers.externalTrigger(
|
||||||
automation,
|
automation,
|
||||||
{ ...prepareTestInput(body), appId, user },
|
{ ...{ ...input, ...(table ? { table } : {}) }, appId, user },
|
||||||
{ getResponses: true }
|
{ getResponses: true }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import { configs, context, events } from "@budibase/backend-core"
|
import { configs, context, events } from "@budibase/backend-core"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import {
|
import {
|
||||||
|
AutomationResults,
|
||||||
ConfigType,
|
ConfigType,
|
||||||
FieldType,
|
FieldType,
|
||||||
FilterCondition,
|
FilterCondition,
|
||||||
|
@ -621,7 +622,7 @@ describe("/automations", () => {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const res = await config.api.automation.test(automation._id!, {
|
const response = await config.api.automation.test(automation._id!, {
|
||||||
fields: {},
|
fields: {},
|
||||||
oldRow: {
|
oldRow: {
|
||||||
City: oldCity,
|
City: oldCity,
|
||||||
|
@ -631,12 +632,14 @@ describe("/automations", () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isDidNotTriggerResponse(res)) {
|
if (isDidNotTriggerResponse(response)) {
|
||||||
throw new Error("Automation did not trigger")
|
throw new Error("Automation did not trigger")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const results: AutomationResults = response as AutomationResults
|
||||||
|
|
||||||
const expectedResult = oldCity === newCity
|
const expectedResult = oldCity === newCity
|
||||||
expect(res.steps[1].outputs.result).toEqual(expectedResult)
|
expect(results.steps[1].outputs.result).toEqual(expectedResult)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -723,7 +726,8 @@ describe("/automations", () => {
|
||||||
if (isDidNotTriggerResponse(res)) {
|
if (isDidNotTriggerResponse(res)) {
|
||||||
expect(expectToRun).toEqual(false)
|
expect(expectToRun).toEqual(false)
|
||||||
} else {
|
} else {
|
||||||
expect(res.steps[1].outputs.success).toEqual(expectToRun)
|
const results: AutomationResults = res as AutomationResults
|
||||||
|
expect(results.steps[1].outputs.success).toEqual(expectToRun)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,7 +12,7 @@ describe("Webhook trigger test", () => {
|
||||||
|
|
||||||
async function createWebhookAutomation() {
|
async function createWebhookAutomation() {
|
||||||
const { automation } = await createAutomationBuilder(config)
|
const { automation } = await createAutomationBuilder(config)
|
||||||
.onWebhook({ fields: { parameter: "string" } })
|
.onWebhook({ body: { parameter: "string" } })
|
||||||
.createRow({
|
.createRow({
|
||||||
row: { tableId: table._id!, name: "{{ trigger.parameter }}" },
|
row: { tableId: table._id!, name: "{{ trigger.parameter }}" },
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { TRIGGER_DEFINITIONS } from "../../triggers"
|
||||||
import {
|
import {
|
||||||
Automation,
|
Automation,
|
||||||
AutomationActionStepId,
|
AutomationActionStepId,
|
||||||
|
AutomationResults,
|
||||||
AutomationStep,
|
AutomationStep,
|
||||||
AutomationStepInputs,
|
AutomationStepInputs,
|
||||||
AutomationTrigger,
|
AutomationTrigger,
|
||||||
|
@ -213,10 +214,11 @@ class AutomationRunner<TStep extends AutomationTriggerStepId> {
|
||||||
throw new Error(response.message)
|
throw new Error(response.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const results: AutomationResults = response as AutomationResults
|
||||||
// Remove the trigger step from the response.
|
// Remove the trigger step from the response.
|
||||||
response.steps.shift()
|
results.steps.shift()
|
||||||
|
|
||||||
return response
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
async trigger(
|
async trigger(
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
UserBindings,
|
UserBindings,
|
||||||
AutomationResults,
|
AutomationResults,
|
||||||
DidNotTriggerResponse,
|
DidNotTriggerResponse,
|
||||||
|
Table,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { executeInThread } from "../threads/automation"
|
import { executeInThread } from "../threads/automation"
|
||||||
import { dataFilters, sdk } from "@budibase/shared-core"
|
import { dataFilters, sdk } from "@budibase/shared-core"
|
||||||
|
@ -154,6 +155,7 @@ interface AutomationTriggerParams {
|
||||||
timeout?: number
|
timeout?: number
|
||||||
appId?: string
|
appId?: string
|
||||||
user?: UserBindings
|
user?: UserBindings
|
||||||
|
table?: Table
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function externalTrigger(
|
export async function externalTrigger(
|
||||||
|
|
|
@ -168,6 +168,7 @@ class S3Integration implements IntegrationBase {
|
||||||
secretAccessKey: config.secretAccessKey,
|
secretAccessKey: config.secretAccessKey,
|
||||||
},
|
},
|
||||||
region: config.region,
|
region: config.region,
|
||||||
|
endpoint: config.endpoint,
|
||||||
}
|
}
|
||||||
if (config.endpoint) {
|
if (config.endpoint) {
|
||||||
this.config.forcePathStyle = true
|
this.config.forcePathStyle = true
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { AutomationJob } from "../../../sdk/automations"
|
||||||
import {
|
import {
|
||||||
Automation,
|
Automation,
|
||||||
AutomationActionStepId,
|
AutomationActionStepId,
|
||||||
|
@ -78,10 +79,25 @@ export interface TestAutomationRequest {
|
||||||
row?: Row
|
row?: Row
|
||||||
oldRow?: Row
|
oldRow?: Row
|
||||||
}
|
}
|
||||||
export type TestAutomationResponse = AutomationResults | DidNotTriggerResponse
|
|
||||||
|
|
||||||
export function isDidNotTriggerResponse(
|
export function isDidNotTriggerResponse(
|
||||||
response: TestAutomationResponse
|
response: TestAutomationResponse
|
||||||
): response is DidNotTriggerResponse {
|
): response is DidNotTriggerResponse {
|
||||||
return !!("message" in response && response.message)
|
return !!("message" in response && response.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAutomationResults(
|
||||||
|
response: TestAutomationResponse
|
||||||
|
): response is AutomationResults {
|
||||||
|
return !!(
|
||||||
|
"steps" in response &&
|
||||||
|
response.steps &&
|
||||||
|
"trigger" in response &&
|
||||||
|
response.trigger
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TestAutomationResponse =
|
||||||
|
| AutomationResults
|
||||||
|
| DidNotTriggerResponse
|
||||||
|
| AutomationJob
|
||||||
|
|
|
@ -11,7 +11,7 @@ export interface SaveQueryRequest extends Query {}
|
||||||
export interface SaveQueryResponse extends Query {}
|
export interface SaveQueryResponse extends Query {}
|
||||||
|
|
||||||
export interface ImportRestQueryRequest {
|
export interface ImportRestQueryRequest {
|
||||||
datasourceId: string
|
datasourceId?: string
|
||||||
data: string
|
data: string
|
||||||
datasource: Datasource
|
datasource: Datasource
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Table } from "@budibase/types"
|
||||||
import { SortOrder } from "../../../api"
|
import { SortOrder } from "../../../api"
|
||||||
import {
|
import {
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
|
@ -305,6 +306,7 @@ export type RowUpdatedTriggerOutputs = {
|
||||||
row: Row
|
row: Row
|
||||||
id: string
|
id: string
|
||||||
revision?: string
|
revision?: string
|
||||||
|
oldRow?: Row
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WebhookTriggerInputs = {
|
export type WebhookTriggerInputs = {
|
||||||
|
@ -312,6 +314,17 @@ export type WebhookTriggerInputs = {
|
||||||
triggerUrl: string
|
triggerUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WebhookTriggerOutputs = {
|
export type WebhookTriggerOutputs = Record<string, any> & {
|
||||||
fields: Record<string, any>
|
body: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RowActionTriggerInputs = {
|
||||||
|
tableId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RowActionTriggerOutputs = {
|
||||||
|
row: Row
|
||||||
|
id: string
|
||||||
|
revision?: string
|
||||||
|
table: Table
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,15 +136,7 @@ export interface Automation extends Document {
|
||||||
internal?: boolean
|
internal?: boolean
|
||||||
type?: string
|
type?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
testData?: {
|
testData?: AutomationTriggerResultOutputs
|
||||||
row?: Row
|
|
||||||
meta: {
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
id: string
|
|
||||||
revision: string
|
|
||||||
oldRow?: Row
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseIOStructure {
|
export interface BaseIOStructure {
|
||||||
|
|
Loading…
Reference in New Issue