Add bindings support to meeting start / end dates (#11759)

* Deprecate zapier+make values1..5

* Allow dates in meeting invite to be bindable

* Add DrawerBindableSlot component

* Show calendar icon

* refactor

* Use cancel icon

* Disable cancel for JS bindings

* WIP

* WIP

* WIP

* Fix linkedIds svelte binding

* Label and icon update

* Handle arrays

* Support text area

* Add reactivity to text area icon

* Support boolean

* JSON support

* Remove rowControl

* Allow boolean field to have three states

* lint

* Refactor

---------

Co-authored-by: Michael Drury <me@michaeldrury.co.uk>
This commit is contained in:
melohagan 2023-09-29 10:15:36 +01:00 committed by GitHub
parent 2868cd9b2b
commit b4e0d98973
9 changed files with 376 additions and 246 deletions

View File

@ -221,18 +221,6 @@ const automationActions = store => ({
newAutomation.definition.steps.splice(blockIdx, 0, block) newAutomation.definition.steps.splice(blockIdx, 0, block)
await store.actions.save(newAutomation) await store.actions.save(newAutomation)
}, },
/**
* "rowControl" appears to be the name of the flag used to determine whether
* a certain automation block uses values or bindings as inputs
*/
toggleRowControl: async (block, rowControl) => {
const newBlock = { ...block, rowControl }
const newAutomation = store.actions.getUpdatedDefinition(
get(selectedAutomation),
newBlock
)
await store.actions.save(newAutomation)
},
deleteAutomationBlock: async block => { deleteAutomationBlock: async block => {
const automation = get(selectedAutomation) const automation = get(selectedAutomation)
let newAutomation = cloneDeep(automation) let newAutomation = cloneDeep(automation)

View File

@ -7,7 +7,6 @@
Detail, Detail,
Modal, Modal,
Button, Button,
Select,
ActionButton, ActionButton,
notifications, notifications,
Label, Label,
@ -39,9 +38,6 @@
step => step.stepId === ActionStepID.COLLECT step => step.stepId === ActionStepID.COLLECT
) )
$: automationId = $selectedAutomation?._id $: automationId = $selectedAutomation?._id
$: showBindingPicker =
block.stepId === ActionStepID.CREATE_ROW ||
block.stepId === ActionStepID.UPDATE_ROW
$: isTrigger = block.type === "TRIGGER" $: isTrigger = block.type === "TRIGGER"
$: steps = $selectedAutomation?.definition?.steps ?? [] $: steps = $selectedAutomation?.definition?.steps ?? []
$: blockIdx = steps.findIndex(step => step.id === block.id) $: blockIdx = steps.findIndex(step => step.id === block.id)
@ -96,15 +92,6 @@
} }
} }
/**
* "rowControl" appears to be the name of the flag used to determine whether
* a certain automation block uses values or bindings as inputs
*/
function toggleRowControl(evt) {
const rowControl = evt.detail !== "Use values"
automationStore.actions.toggleRowControl(block, rowControl)
}
async function addLooping() { async function addLooping() {
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
const loopBlock = automationStore.actions.constructBlock( const loopBlock = automationStore.actions.constructBlock(
@ -189,16 +176,6 @@
Add Looping Add Looping
</ActionButton> </ActionButton>
{/if} {/if}
{#if showBindingPicker}
<Select
on:change={toggleRowControl}
defaultValue="Use values"
autoWidth
value={block.rowControl ? "Use bindings" : "Use values"}
options={["Use values", "Use bindings"]}
placeholder={null}
/>
{/if}
<ActionButton <ActionButton
on:click={() => deleteStep()} on:click={() => deleteStep()}
icon="DeleteOutline" icon="DeleteOutline"

View File

@ -23,6 +23,7 @@
import { environment, licensing } from "stores/portal" import { environment, licensing } from "stores/portal"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte" import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import CodeEditorModal from "./CodeEditorModal.svelte" import CodeEditorModal from "./CodeEditorModal.svelte"
import QuerySelector from "./QuerySelector.svelte" import QuerySelector from "./QuerySelector.svelte"
@ -82,33 +83,6 @@
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])] ? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
: [] : []
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
let deprecatedSchemaProperties
$: {
if (block?.stepId === "integromat" || block?.stepId === "zapier") {
deprecatedSchemaProperties = schemaProperties.filter(
prop => !prop[0].startsWith("value")
)
if (!deprecatedSchemaProperties.map(entry => entry[0]).includes("body")) {
deprecatedSchemaProperties.push([
"body",
{
title: "Payload",
type: "json",
},
])
}
} else {
deprecatedSchemaProperties = schemaProperties
}
}
/****************************************************/
const getInputData = (testData, blockInputs) => { const getInputData = (testData, blockInputs) => {
// Test data is not cloned for reactivity // Test data is not cloned for reactivity
let newInputData = testData || cloneDeep(blockInputs) let newInputData = testData || cloneDeep(blockInputs)
@ -118,30 +92,6 @@
newInputData = cloneDeep(blockInputs) newInputData = cloneDeep(blockInputs)
} }
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
if (
(block?.stepId === "integromat" || block?.stepId === "zapier") &&
!newInputData?.body?.value
) {
let deprecatedValues = {
...newInputData,
}
delete deprecatedValues.url
delete deprecatedValues.body
newInputData = {
url: newInputData.url,
body: {
value: JSON.stringify(deprecatedValues),
},
}
}
/**********************************/
inputData = newInputData inputData = newInputData
setDefaultEnumValues() setDefaultEnumValues()
} }
@ -337,7 +287,7 @@
</script> </script>
<div class="fields"> <div class="fields">
{#each deprecatedSchemaProperties as [key, value]} {#each schemaProperties as [key, value]}
{#if canShowField(key, value)} {#if canShowField(key, value)}
<div class="block-field"> <div class="block-field">
{#if key !== "fields" && value.type !== "boolean"} {#if key !== "fields" && value.type !== "boolean"}
@ -362,18 +312,6 @@
mode="json" mode="json"
value={inputData[key]?.value} value={inputData[key]?.value}
on:change={e => { on:change={e => {
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
delete inputData.value1
delete inputData.value2
delete inputData.value3
delete inputData.value4
delete inputData.value5
/***********************/
onChange(e, key) onChange(e, key)
}} }}
/> />
@ -386,10 +324,23 @@
/> />
</div> </div>
{:else if value.type === "date"} {:else if value.type === "date"}
<DrawerBindableSlot
fillWidth
title={value.title}
panel={AutomationBindingPanel}
type={"date"}
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
allowJS={true}
updateOnChange={false}
drawerLeft="260px"
>
<DatePicker <DatePicker
value={inputData[key]} value={inputData[key]}
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
/> />
</DrawerBindableSlot>
{:else if value.customType === "column"} {:else if value.customType === "column"}
<Select <Select
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
@ -469,7 +420,6 @@
/> />
{:else if value.customType === "row"} {:else if value.customType === "row"}
<RowSelector <RowSelector
{block}
value={inputData[key]} value={inputData[key]}
meta={inputData["meta"] || {}} meta={inputData["meta"] || {}}
on:change={e => { on:change={e => {

View File

@ -1,18 +1,14 @@
<script> <script>
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { Select, Checkbox } from "@budibase/bbui" import { Select, Checkbox } from "@budibase/bbui"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import RowSelectorTypes from "./RowSelectorTypes.svelte" import RowSelectorTypes from "./RowSelectorTypes.svelte"
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value export let value
export let meta export let meta
export let bindings export let bindings
export let block
export let isTestModal export let isTestModal
export let isUpdateRow export let isUpdateRow
@ -25,16 +21,6 @@
let table let table
let schemaFields let schemaFields
let placeholders = {
number: 10,
boolean: "true",
datetime: "2022-02-16T12:00:00.000Z ",
options: "1",
array: "1 2 3 4",
link: "ro_ta_123_456",
longform: "long form text",
}
$: rowControl = block.rowControl
$: { $: {
table = $tables.list.find(table => table._id === value?.tableId) table = $tables.list.find(table => table._id === value?.tableId)
schemaFields = Object.entries(table?.schema ?? {}) schemaFields = Object.entries(table?.schema ?? {})
@ -57,19 +43,13 @@
return value return value
} }
if (type === "boolean") {
if (typeof value === "boolean") {
return value
}
return value === "true"
}
if (type === "number") { if (type === "number") {
if (typeof value === "number") { if (typeof value === "number") {
return value return value
} }
return Number(value) return Number(value)
} }
if (type === "options") { if (type === "options" || type === "boolean") {
return value return value
} }
if (type === "array") { if (type === "array") {
@ -127,9 +107,7 @@
{#if schemaFields.length} {#if schemaFields.length}
<div class="schema-fields"> <div class="schema-fields">
{#each schemaFields as [field, schema]} {#each schemaFields as [field, schema]}
{#if !schema.autocolumn} {#if !schema.autocolumn && schema.type !== "attachment"}
{#if schema.type !== "attachment"}
{#if !rowControl}
<RowSelectorTypes <RowSelectorTypes
{isTestModal} {isTestModal}
{field} {field}
@ -138,23 +116,7 @@
{value} {value}
{onChange} {onChange}
/> />
{:else} {/if}
<div>
<svelte:component
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
placeholder={placeholders[schema.type]}
panel={AutomationBindingPanel}
value={Array.isArray(value[field])
? value[field].join(",")
: value[field]}
on:change={e => onChange(e, field, schema.type)}
label={field}
type="string"
bindings={parsedBindings}
fillWidth={true}
allowJS={true}
updateOnChange={false}
/>
{#if isUpdateRow && schema.type === "link"} {#if isUpdateRow && schema.type === "link"}
<div class="checkbox-field"> <div class="checkbox-field">
<Checkbox <Checkbox
@ -165,10 +127,6 @@
/> />
</div> </div>
{/if} {/if}
</div>
{/if}
{/if}
{/if}
{/each} {/each}
</div> </div>
{/if} {/if}

View File

@ -1,7 +1,6 @@
<script> <script>
import { import {
Select, Select,
Toggle,
DatePicker, DatePicker,
Multiselect, Multiselect,
TextArea, TextArea,
@ -9,6 +8,7 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte" import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte" import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import Editor from "components/integration/QueryEditor.svelte" import Editor from "components/integration/QueryEditor.svelte"
@ -31,39 +31,57 @@
} }
</script> </script>
{#if schemaHasOptions(schema) && schema.type !== "array"} <DrawerBindableSlot
fillWidth
title={value.title}
label={field}
panel={AutomationBindingPanel}
type={schema.type}
{schema}
value={value[field]}
on:change={e => onChange(e, field)}
{bindings}
allowJS={true}
updateOnChange={false}
drawerLeft="260px"
>
{#if schemaHasOptions(schema) && schema.type !== "array"}
<Select <Select
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
label={field} label={field}
value={value[field]} value={value[field]}
options={schema.constraints.inclusion} options={schema.constraints.inclusion}
/> />
{:else if schema.type === "datetime"} {:else if schema.type === "datetime"}
<DatePicker <DatePicker
label={field} label={field}
value={value[field]} value={value[field]}
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
/> />
{:else if schema.type === "boolean"} {:else if schema.type === "boolean"}
<Toggle <Select
text={field}
value={value[field]}
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
label={field}
value={value[field]}
options={[
{ label: "True", value: "true" },
{ label: "False", value: "false" },
]}
/> />
{:else if schema.type === "array"} {:else if schema.type === "array"}
<Multiselect <Multiselect
bind:value={value[field]} bind:value={value[field]}
label={field} label={field}
options={schema.constraints.inclusion} options={schema.constraints.inclusion}
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
/> />
{:else if schema.type === "longform"} {:else if schema.type === "longform"}
<TextArea <TextArea
label={field} label={field}
bind:value={value[field]} bind:value={value[field]}
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
/> />
{:else if schema.type === "json"} {:else if schema.type === "json"}
<span> <span>
<Label>{field}</Label> <Label>{field}</Label>
<Editor <Editor
@ -77,13 +95,13 @@
value={value[field]} value={value[field]}
/> />
</span> </span>
{:else if schema.type === "link"} {:else if schema.type === "link"}
<LinkedRowSelector <LinkedRowSelector
bind:linkedRows={value[field]} bind:linkedRows={value[field]}
{schema} {schema}
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
/> />
{:else if schema.type === "string" || schema.type === "number"} {:else if schema.type === "string" || schema.type === "number"}
<svelte:component <svelte:component
this={isTestModal ? ModalBindableInput : DrawerBindableInput} this={isTestModal ? ModalBindableInput : DrawerBindableInput}
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
@ -96,4 +114,5 @@
allowJS={true} allowJS={true}
updateOnChange={false} updateOnChange={false}
/> />
{/if} {/if}
</DrawerBindableSlot>

View File

@ -11,11 +11,11 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let rows = [] let rows = []
let linkedIds = (Array.isArray(linkedRows) ? linkedRows : [])?.map( let linkedIds = []
$: linkedIds = (Array.isArray(linkedRows) ? linkedRows : [])?.map(
row => row?._id || row row => row?._id || row
) )
$: linkedRows = linkedIds
$: label = capitalise(schema.name) $: label = capitalise(schema.name)
$: linkedTableId = schema.tableId $: linkedTableId = schema.tableId
$: linkedTable = $tables.list.find(table => table._id === linkedTableId) $: linkedTable = $tables.list.find(table => table._id === linkedTableId)

View File

@ -0,0 +1,250 @@
<script>
import { Icon, Input, Drawer, Button } from "@budibase/bbui"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { createEventDispatcher, setContext } from "svelte"
import { isJSBinding } from "@budibase/string-templates"
export let panel = ClientBindingPanel
export let value = ""
export let bindings = []
export let title = "Bindings"
export let placeholder
export let label
export let disabled = false
export let fillWidth
export let allowJS = true
export let allowHelpers = true
export let updateOnChange = true
export let drawerLeft
export let type
export let schema
const dispatch = createEventDispatcher()
let bindingDrawer
let valid = true
let currentVal = value
$: readableValue = runtimeToReadableBinding(bindings, value)
$: tempValue = readableValue
$: isJS = isJSBinding(value)
const saveBinding = () => {
onChange(tempValue)
onBlur()
bindingDrawer.hide()
}
setContext("binding-drawer-actions", {
save: saveBinding,
})
const onChange = value => {
if (type === "link" && value && hasValidLinks(value)) {
currentVal = value.split(",")
} else if (type === "array" && value && hasValidOptions(value)) {
currentVal = value.split(",")
} else {
currentVal = readableToRuntimeBinding(bindings, value)
}
dispatch("change", currentVal)
}
const onBlur = () => {
dispatch("blur", currentVal)
}
const isValidDate = value => {
return !value || !isNaN(new Date(value).valueOf())
}
const hasValidLinks = value => {
let links = []
if (Array.isArray(value)) {
links = value
} else if (value && typeof value === "string") {
links = value.split(",")
} else {
return !value
}
return links.every(link => link.startsWith("ro_"))
}
const hasValidOptions = value => {
let links = []
if (Array.isArray(value)) {
links = value
} else if (value && typeof value === "string") {
links = value.split(",")
} else {
return !value
}
return links.every(link => schema?.constraints?.inclusion?.includes(link))
}
const isValidBoolean = value => {
return value === "false" || value === "true" || value == ""
}
const validationMap = {
date: isValidDate,
datetime: isValidDate,
link: hasValidLinks,
array: hasValidOptions,
longform: value => !isJSBinding(value),
json: value => !isJSBinding(value),
boolean: isValidBoolean,
}
const isValid = value => {
const validate = validationMap[type]
return validate ? validate(value) : true
}
const getIconClass = (value, type) => {
if (type === "longform" && !isJSBinding(value)) {
return "text-area-slot-icon"
}
if (type === "json" && !isJSBinding(value)) {
return "json-slot-icon"
}
if (type !== "string" && type !== "number") {
return "slot-icon"
}
return ""
}
</script>
<div class="control" class:disabled>
{#if !isValid(value)}
<Input
{label}
{disabled}
readonly={isJS}
value={isJS ? "(JavaScript function)" : readableValue}
on:change={event => onChange(event.detail)}
on:blur={onBlur}
{placeholder}
{updateOnChange}
/>
<div
class="icon"
on:click={() => {
if (!isJS) {
dispatch("change", "")
}
}}
>
<Icon disabled={isJS} size="S" name="Close" />
</div>
{:else}
<slot
{label}
{disabled}
readonly={isJS}
value={isJS ? "(JavaScript function)" : readableValue}
{placeholder}
{updateOnChange}
/>
{/if}
{#if !disabled}
<div
class={`icon ${getIconClass(value, type)}`}
on:click={() => {
bindingDrawer.show()
}}
>
<Icon size="S" name="FlashOn" />
</div>
{/if}
</div>
<Drawer
on:drawerHide
on:drawerShow
{fillWidth}
bind:this={bindingDrawer}
{title}
left={drawerLeft}
headless
>
<svelte:fragment slot="description">
Add the objects on the left to enrich your text.
</svelte:fragment>
<Button cta slot="buttons" disabled={!valid} on:click={saveBinding}>
Save
</Button>
<svelte:component
this={panel}
slot="body"
bind:valid
value={readableValue}
on:change={event => (tempValue = event.detail)}
{bindings}
{allowJS}
{allowHelpers}
/>
</Drawer>
<style>
.control {
flex: 1;
position: relative;
}
.slot-icon {
right: 31px !important;
border-right: 1px solid var(--spectrum-alias-border-color);
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
.text-area-slot-icon {
border-bottom: 1px solid var(--spectrum-alias-border-color);
border-bottom-right-radius: 0px !important;
top: 26px !important;
}
.json-slot-icon {
border-bottom: 1px solid var(--spectrum-alias-border-color);
border-bottom-right-radius: 0px !important;
top: 23px !important;
right: 0px !important;
}
.icon {
right: 1px;
bottom: 1px;
position: absolute;
justify-content: center;
align-items: center;
display: flex;
flex-direction: row;
box-sizing: border-box;
border-left: 1px solid var(--spectrum-alias-border-color);
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
width: 31px;
color: var(--spectrum-alias-text-color);
background-color: var(--spectrum-global-color-gray-75);
transition: background-color
var(--spectrum-global-animation-duration-100, 130ms),
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
border-color var(--spectrum-global-animation-duration-100, 130ms);
height: calc(var(--spectrum-alias-item-height-m) - 2px);
}
.icon:hover {
cursor: pointer;
color: var(--spectrum-alias-text-color-hover);
background-color: var(--spectrum-global-color-gray-50);
border-color: var(--spectrum-alias-border-color-hover);
}
.control:not(.disabled) :global(.spectrum-Textfield-input) {
padding-right: 40px;
}
</style>

View File

@ -78,8 +78,7 @@ export const definition: AutomationStepSchema = {
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({ inputs }: AutomationStepInput) {
//TODO - Remove deprecated values 1,2,3,4,5 after November 2023 const { url, body } = inputs
const { url, value1, value2, value3, value4, value5, body } = inputs
let payload = {} let payload = {}
try { try {
@ -104,11 +103,6 @@ export async function run({ inputs }: AutomationStepInput) {
response = await fetch(url, { response = await fetch(url, {
method: "post", method: "post",
body: JSON.stringify({ body: JSON.stringify({
value1,
value2,
value3,
value4,
value5,
...payload, ...payload,
}), }),
headers: { headers: {

View File

@ -71,8 +71,7 @@ export const definition: AutomationStepSchema = {
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({ inputs }: AutomationStepInput) {
//TODO - Remove deprecated values 1,2,3,4,5 after November 2023 const { url, body } = inputs
const { url, value1, value2, value3, value4, value5, body } = inputs
let payload = {} let payload = {}
try { try {
@ -100,11 +99,6 @@ export async function run({ inputs }: AutomationStepInput) {
method: "post", method: "post",
body: JSON.stringify({ body: JSON.stringify({
platform: "budibase", platform: "budibase",
value1,
value2,
value3,
value4,
value5,
...payload, ...payload,
}), }),
headers: { headers: {