diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js
index 2cb681670b..01555446d9 100644
--- a/packages/bbui/src/Actions/position_dropdown.js
+++ b/packages/bbui/src/Actions/position_dropdown.js
@@ -56,6 +56,8 @@ export default function positionDropdown(element, opts) {
styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width
} else if (align === "right-outside") {
styles.left = anchorBounds.right + offset
+ } else if (align === "left-outside") {
+ styles.left = anchorBounds.left - elementBounds.width - offset
} else {
styles.left = anchorBounds.left
}
diff --git a/packages/bbui/src/Drawer/Drawer.svelte b/packages/bbui/src/Drawer/Drawer.svelte
index 932236bc0c..18ac15dffa 100644
--- a/packages/bbui/src/Drawer/Drawer.svelte
+++ b/packages/bbui/src/Drawer/Drawer.svelte
@@ -3,11 +3,13 @@
import Button from "../Button/Button.svelte"
import Body from "../Typography/Body.svelte"
import Heading from "../Typography/Heading.svelte"
+ import { setContext } from "svelte"
export let title
export let fillWidth
export let left = "314px"
export let width = "calc(100% - 626px)"
+ export let headless = false
let visible = false
@@ -25,6 +27,11 @@
visible = false
}
+ setContext("drawer-actions", {
+ hide,
+ show,
+ })
+
const easeInOutQuad = x => {
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2
}
@@ -50,18 +57,20 @@
transition:slide|local
style={`width: ${width}; left: ${left};`}
>
-
-
- {title}
-
-
-
-
-
-
-
-
-
+ {#if !headless}
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+ {/if}
diff --git a/packages/bbui/src/Tabs/Tabs.svelte b/packages/bbui/src/Tabs/Tabs.svelte
index 7184aedbaf..4cc22b226e 100644
--- a/packages/bbui/src/Tabs/Tabs.svelte
+++ b/packages/bbui/src/Tabs/Tabs.svelte
@@ -12,6 +12,7 @@
export let emphasized = false
export let onTop = false
export let size = "M"
+ export let beforeSwitch = null
let thisSelected = undefined
@@ -28,9 +29,14 @@
thisSelected = selected
dispatch("select", thisSelected)
} else if ($tab.title !== thisSelected) {
- thisSelected = $tab.title
- selected = $tab.title
- dispatch("select", thisSelected)
+ if (typeof beforeSwitch == "function") {
+ const proceed = beforeSwitch($tab.title)
+ if (proceed) {
+ thisSelected = $tab.title
+ selected = $tab.title
+ dispatch("select", thisSelected)
+ }
+ }
}
if ($tab.title !== thisSelected) {
tab.update(state => {
diff --git a/packages/builder/package.json b/packages/builder/package.json
index 561dcf6464..73bc003343 100644
--- a/packages/builder/package.json
+++ b/packages/builder/package.json
@@ -63,6 +63,13 @@
"@budibase/shared-core": "0.0.0",
"@budibase/string-templates": "0.0.0",
"@budibase/types": "0.0.0",
+ "@codemirror/autocomplete": "^6.7.1",
+ "@codemirror/commands": "^6.2.4",
+ "@codemirror/lang-javascript": "^6.1.8",
+ "@codemirror/language": "^6.6.0",
+ "@codemirror/state": "^6.2.0",
+ "@codemirror/theme-one-dark": "^6.1.2",
+ "@codemirror/view": "^6.11.2",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js
index 0d41931a55..d6bc358d39 100644
--- a/packages/builder/src/builderStore/dataBinding.js
+++ b/packages/builder/src/builderStore/dataBinding.js
@@ -77,7 +77,7 @@ export const getAuthBindings = () => {
runtime: `${safeUser}.${safeOAuth2}.${safeAccessToken}`,
readable: `Current User.OAuthToken`,
key: "accessToken",
- display: { name: "OAuthToken" },
+ display: { name: "OAuthToken", type: "text" },
},
]
@@ -434,6 +434,9 @@ export const getUserBindings = () => {
providerId: "user",
category: "Current User",
icon: "User",
+ display: {
+ name: key,
+ },
})
return acc
}, [])
@@ -550,7 +553,7 @@ const getUrlBindings = asset => {
readableBinding: `URL.${param}`,
category: "URL",
icon: "RailTop",
- display: { type: "string" },
+ display: { type: "string", name: param },
}))
const queryParamsBinding = {
type: "context",
@@ -558,7 +561,7 @@ const getUrlBindings = asset => {
readableBinding: "Query params",
category: "URL",
icon: "RailTop",
- display: { type: "object" },
+ display: { type: "object", name: "Query params" },
}
return urlParamBindings.concat([queryParamsBinding])
}
diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js
index 4f2a1bef6f..7f83b2b464 100644
--- a/packages/builder/src/builderStore/store/frontend.js
+++ b/packages/builder/src/builderStore/store/frontend.js
@@ -71,6 +71,7 @@ const INITIAL_FRONTEND_STATE = {
customTheme: {},
previewDevice: "desktop",
highlightedSettingKey: null,
+ propertyFocus: null,
builderSidePanel: false,
hasLock: true,
@@ -1326,6 +1327,12 @@ export const getFrontendStore = () => {
highlightedSettingKey: key,
}))
},
+ propertyFocus: key => {
+ store.update(state => ({
+ ...state,
+ propertyFocus: key,
+ }))
+ },
},
dnd: {
start: component => {
diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
index 35c9c6ad6d..cd0aa1197f 100644
--- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
@@ -29,7 +29,10 @@
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
import { LuceneUtils } from "@budibase/frontend-core"
- import { getSchemaForTable } from "builderStore/dataBinding"
+ import {
+ getSchemaForTable,
+ getEnvironmentBindings,
+ } from "builderStore/dataBinding"
import { Utils } from "@budibase/frontend-core"
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
import { onMount } from "svelte"
@@ -210,6 +213,19 @@
}
const outputs = Object.entries(schema)
+ let bindingIcon = ""
+ let bindindingRank = 0
+
+ if (idx === 0) {
+ bindingIcon = automation.trigger.icon
+ } else if (isLoopBlock) {
+ bindingIcon = "Reuse"
+ bindindingRank = idx + 1
+ } else {
+ bindingIcon = allSteps[idx].icon
+ bindindingRank = idx - loopBlockCount
+ }
+
bindings = bindings.concat(
outputs.map(([name, value]) => {
let runtimeName = isLoopBlock
@@ -218,17 +234,24 @@
? `steps[${idx - loopBlockCount}].${name}`
: `steps.${idx - loopBlockCount}.${name}`
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
+ const categoryName =
+ idx === 0
+ ? "Trigger outputs"
+ : isLoopBlock
+ ? "Loop Outputs"
+ : `Step ${idx - loopBlockCount} outputs`
return {
- label: runtime,
+ readableBinding: runtime,
+ runtimeBinding: runtime,
type: value.type,
description: value.description,
- category:
- idx === 0
- ? "Trigger outputs"
- : isLoopBlock
- ? "Loop Outputs"
- : `Step ${idx - loopBlockCount} outputs`,
- path: runtime,
+ icon: bindingIcon,
+ category: categoryName,
+ display: {
+ type: value.type,
+ name: name,
+ rank: bindindingRank,
+ },
}
})
)
@@ -237,15 +260,12 @@
// Environment bindings
if ($licensing.environmentVariablesEnabled) {
bindings = bindings.concat(
- $environment.variables.map(variable => {
+ getEnvironmentBindings().map(binding => {
return {
- label: `env.${variable.name}`,
- path: `env.${variable.name}`,
- icon: "Key",
- category: "Environment",
+ ...binding,
display: {
- type: "string",
- name: variable.name,
+ ...binding.display,
+ rank: 98,
},
}
})
diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte
new file mode 100644
index 0000000000..b526d5a3b4
--- /dev/null
+++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte
@@ -0,0 +1,287 @@
+
+
+{#if label}
+
+
+
+{/if}
+
+
+
+
diff --git a/packages/builder/src/components/common/CodeEditor/index.js b/packages/builder/src/components/common/CodeEditor/index.js
new file mode 100644
index 0000000000..8815b1aec2
--- /dev/null
+++ b/packages/builder/src/components/common/CodeEditor/index.js
@@ -0,0 +1,373 @@
+import { EditorView } from "@codemirror/view"
+import { getManifest } from "@budibase/string-templates"
+import sanitizeHtml from "sanitize-html"
+import { groupBy } from "lodash"
+
+export const EditorModes = {
+ JS: {
+ name: "javascript",
+ json: false,
+ match: /\$$/,
+ },
+ Handlebars: {
+ name: "handlebars",
+ base: "text/html",
+ match: /{{[\s]*[\w\s]*/,
+ },
+ Text: {
+ name: "text/html",
+ },
+}
+
+export const SECTIONS = {
+ HB_HELPER: {
+ name: "Helper",
+ type: "helper",
+ icon: "Code",
+ },
+}
+
+export const getDefaultTheme = opts => {
+ const { height, resize, dark } = opts
+ return EditorView.theme(
+ {
+ "&.cm-focused .cm-cursor": {
+ borderLeftColor: "var(--spectrum-alias-text-color)",
+ },
+ "&": {
+ height: height ? `${height}` : "",
+ lineHeight: "1.3",
+ border:
+ "var(--spectrum-alias-border-size-thin) solid var(--spectrum-alias-border-color)",
+ borderRadius: "var(--border-radius-s)",
+ backgroundColor:
+ "var( --spectrum-textfield-m-background-color, var(--spectrum-global-color-gray-50) )",
+ resize: resize ? `${resize}` : "",
+ overflow: "hidden",
+ color: "var(--spectrum-alias-text-color)",
+ },
+ "& .cm-tooltip.cm-tooltip-autocomplete > ul": {
+ fontFamily:
+ "var(--spectrum-alias-body-text-font-family, var(--spectrum-global-font-family-base))",
+ maxHeight: "16em",
+ },
+ "& .cm-placeholder": {
+ color: "var(--spectrum-alias-text-color)",
+ fontStyle: "italic",
+ },
+ "&.cm-focused": {
+ outline: "none",
+ borderColor: "var(--spectrum-alias-border-color-mouse-focus)",
+ },
+ // AUTO COMPLETE
+ "& .cm-completionDetail": {
+ fontStyle: "unset",
+ textTransform: "uppercase",
+ fontSize: "10px",
+ },
+ "& .info-bubble": {
+ fontSize: "var(--font-size-s)",
+ display: "grid",
+ gridGap: "var(--spacing-s)",
+ gridTemplateColumns: "1fr",
+ },
+ "& .cm-tooltip": {
+ marginLeft: "var(--spacing-s)",
+ border: "1px solid var(--spectrum-global-color-gray-300)",
+ borderRadius:
+ "var( --spectrum-popover-border-radius, var(--spectrum-alias-border-radius-regular) )",
+ },
+ // Section header
+ "& .info-section": {
+ display: "flex",
+ padding: "var(--spacing-s)",
+ gap: "var(--spacing-m)",
+ borderBottom: "1px solid var(--spectrum-global-color-gray-300)",
+ },
+ // Autocomplete Option
+ "& .cm-tooltip.cm-tooltip-autocomplete .autocomplete-option": {
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+ fontSize: "var(--spectrum-alias-font-size-default)",
+ padding: "var(--spacing-s)",
+ },
+ "& .cm-tooltip-autocomplete ul li[aria-selected].autocomplete-option": {
+ backgroundColor: "var(--spectrum-global-color-gray-200)",
+ color: "var(--ink)",
+ },
+ "& .binding-wrap": {
+ color: "var(--spectrum-global-color-blue-700)",
+ },
+ },
+ { dark }
+ )
+}
+
+export const buildHelperInfoNode = (completion, helper) => {
+ const ele = document.createElement("div")
+ ele.classList.add("info-bubble")
+
+ const exampleNodeHtml = helper.example
+ ? `${helper.example}
`
+ : ""
+ const descriptionMarkup = sanitizeHtml(helper.description, {
+ allowedTags: [],
+ allowedAttributes: {},
+ })
+ const descriptionNodeHtml = `${descriptionMarkup}
`
+
+ ele.innerHTML = `
+ ${exampleNodeHtml}
+ ${descriptionNodeHtml}
+ `
+ return ele
+}
+
+const toSpectrumIcon = name => {
+ return ``
+}
+
+export const buildSectionHeader = (type, sectionName, icon, rank) => {
+ const ele = document.createElement("div")
+ ele.classList.add("info-section")
+ ele.classList.add(type)
+ ele.innerHTML = `${toSpectrumIcon(icon)}${sectionName}`
+ return {
+ name: sectionName,
+ header: () => ele,
+ rank,
+ }
+}
+
+export const helpersToCompletion = (helpers, mode) => {
+ const { type, name: sectionName, icon } = SECTIONS.HB_HELPER
+ const helperSection = buildSectionHeader(type, sectionName, icon, 99)
+
+ return Object.keys(helpers).reduce((acc, key) => {
+ let helper = helpers[key]
+ acc.push({
+ label: key,
+ info: completion => {
+ return buildHelperInfoNode(completion, helper)
+ },
+ type: "helper",
+ section: helperSection,
+ detail: "FUNCTION",
+ apply: (view, completion, from, to) => {
+ insertBinding(view, from, to, key, mode)
+ },
+ })
+ return acc
+ }, [])
+}
+
+export const getHelperCompletions = mode => {
+ const manifest = getManifest()
+ return Object.keys(manifest).reduce((acc, key) => {
+ acc = acc || []
+ return [...acc, ...helpersToCompletion(manifest[key], mode)]
+ }, [])
+}
+
+const bindingFilter = (options, query) => {
+ return options.filter(completion => {
+ const section_parsed = completion.section.name.toLowerCase()
+ const label_parsed = completion.label.toLowerCase()
+ const query_parsed = query.toLowerCase()
+
+ return (
+ section_parsed.includes(query_parsed) ||
+ label_parsed.includes(query_parsed)
+ )
+ })
+}
+
+export const hbAutocomplete = baseCompletions => {
+ async function coreCompletion(context) {
+ let bindingStart = context.matchBefore(EditorModes.Handlebars.match)
+
+ let options = baseCompletions || []
+
+ if (!bindingStart) {
+ return null
+ }
+ // Accommodate spaces
+ const match = bindingStart.text.match(/{{[\s]*/)
+ const query = bindingStart.text.replace(match[0], "")
+ let filtered = bindingFilter(options, query)
+
+ return {
+ from: bindingStart.from + match[0].length,
+ filter: false,
+ options: filtered,
+ }
+ }
+
+ return coreCompletion
+}
+
+export const jsAutocomplete = baseCompletions => {
+ async function coreCompletion(context) {
+ let jsBinding = context.matchBefore(/\$\("[\s\w]*/)
+ let options = baseCompletions || []
+
+ if (jsBinding) {
+ // Accommodate spaces
+ const match = jsBinding.text.match(/\$\("[\s]*/)
+ const query = jsBinding.text.replace(match[0], "")
+ let filtered = bindingFilter(options, query)
+ return {
+ from: jsBinding.from + match[0].length,
+ filter: false,
+ options: filtered,
+ }
+ }
+
+ return null
+ }
+
+ return coreCompletion
+}
+
+export const buildBindingInfoNode = (completion, binding) => {
+ const ele = document.createElement("div")
+ ele.classList.add("info-bubble")
+
+ const exampleNodeHtml = binding.readableBinding
+ ? `{{ ${binding.readableBinding} }}
`
+ : ""
+
+ const descriptionNodeHtml = binding.description
+ ? `${binding.description}
`
+ : ""
+
+ ele.innerHTML = `
+ ${exampleNodeHtml}
+ ${descriptionNodeHtml}
+ `
+ return ele
+}
+
+// Readdress these methods. They shouldn't be used
+export const hbInsert = (value, from, to, text) => {
+ let parsedInsert = ""
+
+ const left = from ? value.substring(0, from) : ""
+ const right = to ? value.substring(to) : ""
+
+ if (!left.includes("{{") || !right.includes("}}")) {
+ parsedInsert = `{{ ${text} }}`
+ } else {
+ parsedInsert = ` ${text} `
+ }
+
+ return parsedInsert
+}
+
+export function jsInsert(value, from, to, text, { helper } = {}) {
+ let parsedInsert = ""
+
+ const left = from ? value.substring(0, from) : ""
+ const right = to ? value.substring(to) : ""
+
+ if (helper) {
+ parsedInsert = `helpers.${text}()`
+ } else if (!left.includes('$("') || !right.includes('")')) {
+ parsedInsert = `$("${text}")`
+ } else {
+ parsedInsert = text
+ }
+
+ return parsedInsert
+}
+
+// Autocomplete apply behaviour
+export const insertBinding = (view, from, to, text, mode) => {
+ let parsedInsert
+
+ if (mode.name == "javascript") {
+ parsedInsert = jsInsert(view.state.doc?.toString(), from, to, text)
+ } else if (mode.name == "handlebars") {
+ parsedInsert = hbInsert(view.state.doc?.toString(), from, to, text)
+ } else {
+ console.log("Unsupported")
+ return
+ }
+
+ let bindingClosePattern = mode.name == "javascript" ? /[\s]*"\)/ : /[\s]*}}/
+ let sliced = view.state.doc?.toString().slice(to)
+
+ const rightBrace = sliced.match(bindingClosePattern)
+ let cursorPos = from + parsedInsert.length
+
+ if (rightBrace) {
+ cursorPos = from + parsedInsert.length + rightBrace[0].length
+ }
+
+ view.dispatch({
+ changes: {
+ from,
+ to,
+ insert: parsedInsert,
+ },
+ selection: {
+ anchor: cursorPos,
+ },
+ })
+}
+
+export const bindingsToCompletions = (bindings, mode) => {
+ const bindingByCategory = groupBy(bindings, "category")
+ const categoryMeta = bindings?.reduce((acc, ele) => {
+ acc[ele.category] = acc[ele.category] || {}
+
+ if (ele.icon) {
+ acc[ele.category]["icon"] = acc[ele.category]["icon"] || ele.icon
+ }
+ if (typeof ele.display?.rank == "number") {
+ acc[ele.category]["rank"] = acc[ele.category]["rank"] || ele.display.rank
+ }
+ return acc
+ }, {})
+
+ const completions = Object.keys(bindingByCategory).reduce((comps, catKey) => {
+ const { icon, rank } = categoryMeta[catKey] || {}
+
+ const bindindSectionHeader = buildSectionHeader(
+ bindingByCategory.type,
+ catKey,
+ icon || "",
+ typeof rank == "number" ? rank : 1
+ )
+
+ return [
+ ...comps,
+ ...bindingByCategory[catKey].reduce((acc, binding) => {
+ let displayType = binding.fieldSchema?.type || binding.display?.type
+ acc.push({
+ label: binding.display?.name || "NO NAME",
+ info: completion => {
+ return buildBindingInfoNode(completion, binding)
+ },
+ type: "binding",
+ detail: displayType,
+ section: bindindSectionHeader,
+ apply: (view, completion, from, to) => {
+ insertBinding(view, from, to, binding.readableBinding, mode)
+ },
+ })
+ return acc
+ }, []),
+ ]
+ }, [])
+
+ return completions
+}
diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte
index 3a1c6c4fee..f74a79c387 100644
--- a/packages/builder/src/components/common/bindings/BindingPanel.svelte
+++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte
@@ -1,17 +1,13 @@
-
-
-
-
- {#if hoverTarget.title}
-
{hoverTarget.title}
- {/if}
- {#if hoverTarget.description}
-
- {@html hoverTarget.description}
-
- {/if}
- {#if hoverTarget.example}
-
{hoverTarget.example}
- {/if}
-
-
-
-
+
+
+
+
{
+ if (selectedMode == mode) {
+ return true
+ }
-
-
-
- {#if selectedCategory}
-
-
{
- selectedCategory = null
- }}
- >
- Back
-
-
- {/if}
+ //Get the current mode value
+ const editorValue = usingJS ? decodeJSBinding(jsValue) : hbsValue
- {#if !selectedCategory}
- Search
-
- {/if}
-
- {#if !selectedCategory && !search}
-
- {#each categoryNames as categoryName}
- - {
- selectedCategory = categoryName
- }}
- >
-
- {categoryName}
-
-
- {/each}
-
- {/if}
-
- {#if selectedCategory || search}
- {#each filteredCategories as category}
- {#if category.bindings?.length}
-
- {category.name}
-
-
- {#each category.bindings as binding}
- - {
- popoverAnchor = e.target
- if (!binding.description) {
- return
- }
- hoverTarget = {
- title: binding.display?.name || binding.fieldSchema?.name,
- description: binding.description,
- }
- popover.show()
- e.stopPropagation()
- }}
- on:mouseleave={() => {
- popover.hide()
- popoverAnchor = null
- hoverTarget = null
- }}
- on:focus={() => {}}
- on:blur={() => {}}
- on:click={() => addBinding(binding)}
- >
-
- {#if binding.display?.name}
- {binding.display.name}
- {:else if binding.fieldSchema?.name}
- {binding.fieldSchema?.name}
- {:else}
- {binding.readableBinding}
- {/if}
-
-
- {#if binding.display?.type || binding.fieldSchema?.type}
-
-
- {binding.display?.type || binding.fieldSchema?.type}
-
-
+ if (editorValue) {
+ targetMode = selectedMode
+ return false
+ }
+ return true
+ }}
+ >
+
+
+
+
+ {#if targetMode}
+
+
+
+ {`Switch to ${targetMode}?`}
+
+ This will discard anything in your binding
+
+
+
+
+
+
+ {/if}
+
+
+
-
+ {#if allowJS}
+
+
+
+
+ {#if targetMode}
+
+
+
+ {`Switch to ${targetMode}?`}
+
+ This will discard anything in your binding
+
+
+
+
+
+
+ {/if}
+
+
+
+
+
+
+ {#if sidebar}
+
+
+
+ {/if}
+
+
+ {/if}
+
+
+
+
+
+
+
+
diff --git a/packages/builder/src/components/common/bindings/BindingPicker.svelte b/packages/builder/src/components/common/bindings/BindingPicker.svelte
new file mode 100644
index 0000000000..0f709fd262
--- /dev/null
+++ b/packages/builder/src/components/common/bindings/BindingPicker.svelte
@@ -0,0 +1,393 @@
+
+
+
+
+
+
+ {#if hoverTarget.title}
+
{hoverTarget.title}
+ {/if}
+ {#if hoverTarget.description}
+
+ {@html hoverTarget.description}
+
+ {/if}
+ {#if hoverTarget.example}
+
{hoverTarget.example}
+ {/if}
+
+
+
+
+
+
+ {#if selectedCategory}
+
+
{
+ selectedCategory = null
+ }}
+ >
+ Back
+
+
+ {/if}
+
+ {#if !selectedCategory}
+
+
+
+
+
+
+ {
+ if (!search) {
+ return
+ }
+ search = null
+ }}
+ class:searching={search}
+ >
+
+
+
+ {/if}
+
+ {#if !selectedCategory && !search}
+
+ {#each categoryNames as categoryName}
+ - {
+ selectedCategory = categoryName
+ }}
+ >
+
+ {categoryName}
+
+
+ {/each}
+
+ {/if}
+
+ {#if selectedCategory || search}
+ {#each filteredCategories as category}
+ {#if category.bindings?.length}
+
+
+ {category.name}
+
+
+ {#each category.bindings as binding}
+
+ - {
+ popoverAnchor = e.target
+ if (!binding.description) {
+ return
+ }
+ hoverTarget = {
+ title: binding.display?.name || binding.fieldSchema?.name,
+ description: binding.description,
+ }
+ popover.show()
+ e.stopPropagation()
+ }}
+ on:mouseleave={() => {
+ popover.hide()
+ popoverAnchor = null
+ hoverTarget = null
+ }}
+ on:focus={() => {}}
+ on:blur={() => {}}
+ on:click={() => addBinding(binding)}
+ >
+
+ {#if binding.display?.name}
+ {binding.display.name}
+ {:else if binding.fieldSchema?.name}
+ {binding.fieldSchema?.name}
+ {:else}
+ {binding.readableBinding}
+ {/if}
+
+
+ {#if binding.display?.type || binding.fieldSchema?.type}
+
+
+ {binding.display?.type || binding.fieldSchema?.type}
+
+
+ {/if}
+
+ {/each}
+
+
+ {/if}
+ {/each}
+
+ {#if selectedCategory === "Helpers" || search}
+ {#if filteredHelpers?.length}
+
+
Helpers
+
+ {#each filteredHelpers as helper}
+ - addHelper(helper, mode.name == "javascript")}
+ on:mouseenter={e => {
+ popoverAnchor = e.target
+ if (!helper.displayText && helper.description) {
+ return
+ }
+ hoverTarget = {
+ title: helper.displayText,
+ description: helper.description,
+ example: getHelperExample(
+ helper,
+ mode.name == "javascript"
+ ),
+ }
+ popover.show()
+ e.stopPropagation()
+ }}
+ on:mouseleave={() => {
+ popover.hide()
+ popoverAnchor = null
+ hoverTarget = null
+ }}
+ on:focus={() => {}}
+ on:blur={() => {}}
+ >
+ {helper.displayText}
+
+ function
+
+
+ {/each}
+
+
+ {/if}
+ {/if}
+ {/if}
+
+
+
diff --git a/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte b/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte
index 9a05b90567..bb01d557b8 100644
--- a/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte
+++ b/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte
@@ -5,7 +5,7 @@
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
- import { createEventDispatcher } from "svelte"
+ import { createEventDispatcher, setContext } from "svelte"
import { isJSBinding } from "@budibase/string-templates"
export let panel = ClientBindingPanel
@@ -34,6 +34,10 @@
bindingDrawer.hide()
}
+ setContext("binding-drawer-actions", {
+ save: handleClose,
+ })
+
const onChange = (value, optionPicked) => {
// Add HBS braces if picking binding
if (optionPicked && !options?.includes(value)) {
@@ -63,7 +67,6 @@
on:pick={e => onChange(e.detail, true)}
on:blur={() => dispatch("blur")}
{placeholder}
- options={allOptions}
{error}
/>
{#if !disabled}
@@ -77,6 +80,7 @@
Add the objects on the left to enrich your text.
+
diff --git a/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte b/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte
index cc64e28835..f41b66bd77 100644
--- a/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte
+++ b/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte
@@ -4,8 +4,11 @@
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
+
+ import { store } from "builderStore"
+
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
- import { createEventDispatcher } from "svelte"
+ import { createEventDispatcher, setContext } from "svelte"
import { isJSBinding } from "@budibase/string-templates"
export let panel = ClientBindingPanel
@@ -20,6 +23,7 @@
export let allowHelpers = true
export let updateOnChange = true
export let drawerLeft
+ export let key
const dispatch = createEventDispatcher()
let bindingDrawer
@@ -32,10 +36,15 @@
const saveBinding = () => {
onChange(tempValue)
+ store.actions.settings.propertyFocus(null)
onBlur()
bindingDrawer.hide()
}
+ setContext("binding-drawer-actions", {
+ save: saveBinding,
+ })
+
const onChange = value => {
currentVal = readableToRuntimeBinding(bindings, value)
dispatch("change", currentVal)
@@ -58,12 +67,24 @@
{updateOnChange}
/>
{#if !disabled}
-
+
{
+ store.actions.settings.propertyFocus(key)
+ bindingDrawer.show()
+ }}
+ >
{/if}
-
+
Add the objects on the left to enrich your text.
diff --git a/packages/builder/src/components/design/settings/componentSettings.js b/packages/builder/src/components/design/settings/componentSettings.js
index 39a82c20dd..31f51bf468 100644
--- a/packages/builder/src/components/design/settings/componentSettings.js
+++ b/packages/builder/src/components/design/settings/componentSettings.js
@@ -17,14 +17,14 @@ import URLSelect from "./controls/URLSelect.svelte"
import OptionsEditor from "./controls/OptionsEditor/OptionsEditor.svelte"
import FormFieldSelect from "./controls/FormFieldSelect.svelte"
import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelte"
-import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
+import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
import BarButtonList from "./controls/BarButtonList.svelte"
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
const componentMap = {
- text: DrawerBindableCombobox,
+ text: DrawerBindableInput,
select: Select,
radio: RadioGroup,
dataSource: DataSourceSelect,
diff --git a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte
index 30a05d4c6e..daa3f29592 100644
--- a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte
+++ b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte
@@ -21,6 +21,7 @@
export let componentBindings = []
export let nested = false
export let highlighted = false
+ export let propertyFocus = false
export let info = null
$: nullishValue = value == null || value === ""
@@ -72,6 +73,10 @@
if (highlighted) {
store.actions.settings.highlight(null)
}
+ // To fix focus 'affect' when property is target of a drawer other actions in the builder.
+ if (propertyFocus) {
+ store.actions.settings.propertyFocus(null)
+ }
})
@@ -79,6 +84,7 @@
class="property-control"
class:wide={!label || labelHidden}
class:highlighted={highlighted && nullishValue}
+ class:property-focus={propertyFocus}
>
{#if label && !labelHidden}
@@ -125,6 +131,14 @@
background: var(--spectrum-global-color-gray-300);
border-color: var(--spectrum-global-color-static-red-600);
}
+
+ .property-control.property-focus :global(input) {
+ border-color: var(
+ --spectrum-textfield-m-border-color-down,
+ var(--spectrum-alias-border-color-mouse-focus)
+ );
+ }
+
.label {
margin-top: 16px;
transform: translateY(-50%);
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentKeyHandler.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentKeyHandler.svelte
index 956e1f7a3b..e1b85b6f3f 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentKeyHandler.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentKeyHandler.svelte
@@ -101,7 +101,12 @@
}
// Ignore events when typing
const activeTag = document.activeElement?.tagName.toLowerCase()
- if (["input", "textarea"].indexOf(activeTag) !== -1 && e.key !== "Escape") {
+ const inCodeEditor =
+ document.activeElement?.classList?.contains("cm-content")
+ if (
+ (inCodeEditor || ["input", "textarea"].indexOf(activeTag) !== -1) &&
+ e.key !== "Escape"
+ ) {
return
}
// Key events are always for the selected component
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte
index 8fa0af45ed..78eecea76e 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte
@@ -140,6 +140,7 @@
nested={setting.nested}
onChange={val => updateSetting(setting, val)}
highlighted={$store.highlightedSettingKey === setting.key}
+ propertyFocus={$store.propertyFocus === setting.key}
info={setting.info}
props={{
// Generic settings
diff --git a/yarn.lock b/yarn.lock
index d932fa7437..2f58e5fa66 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1889,6 +1889,84 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
+"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.7.1":
+ version "6.7.1"
+ resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.7.1.tgz#3364799b78dff70fb8f81615536c52ea53ce40b2"
+ integrity sha512-hSxf9S0uB+GV+gBsjY1FZNo53e1FFdzPceRfCfD1gWOnV6o21GfB5J5Wg9G/4h76XZMPrF0A6OCK/Rz5+V1egg==
+ dependencies:
+ "@codemirror/language" "^6.0.0"
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.6.0"
+ "@lezer/common" "^1.0.0"
+
+"@codemirror/commands@^6.2.4":
+ version "6.2.4"
+ resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.2.4.tgz#b8a0e5ce72448c092ba4c4b1d902e6f183948aec"
+ integrity sha512-42lmDqVH0ttfilLShReLXsDfASKLXzfyC36bzwcqzox9PlHulMcsUOfHXNo2X2aFMVNUoQ7j+d4q5bnfseYoOA==
+ dependencies:
+ "@codemirror/language" "^6.0.0"
+ "@codemirror/state" "^6.2.0"
+ "@codemirror/view" "^6.0.0"
+ "@lezer/common" "^1.0.0"
+
+"@codemirror/lang-javascript@^6.1.8":
+ version "6.1.8"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-javascript/-/lang-javascript-6.1.8.tgz#2d0f7de0175f7ad05b6a71d3fe580809b884b339"
+ integrity sha512-5cIA6IOkslTu1DtldcYnj7hsBm3p+cD37qSaKvW1kV16M6q9ysKvKrveCOWgbrj4+ilSWRL2JtSLudbeB158xg==
+ dependencies:
+ "@codemirror/autocomplete" "^6.0.0"
+ "@codemirror/language" "^6.6.0"
+ "@codemirror/lint" "^6.0.0"
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.0.0"
+ "@lezer/common" "^1.0.0"
+ "@lezer/javascript" "^1.0.0"
+
+"@codemirror/language@^6.0.0", "@codemirror/language@^6.6.0":
+ version "6.6.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.6.0.tgz#2204407174a38a68053715c19e28ad61f491779f"
+ integrity sha512-cwUd6lzt3MfNYOobdjf14ZkLbJcnv4WtndYaoBkbor/vF+rCNguMPK0IRtvZJG4dsWiaWPcK8x1VijhvSxnstg==
+ dependencies:
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.0.0"
+ "@lezer/common" "^1.0.0"
+ "@lezer/highlight" "^1.0.0"
+ "@lezer/lr" "^1.0.0"
+ style-mod "^4.0.0"
+
+"@codemirror/lint@^6.0.0":
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.2.1.tgz#654581d8cc293c315ecfa5c9d61d78c52bbd9ccd"
+ integrity sha512-y1muai5U/uUPAGRyHMx9mHuHLypPcHWxzlZGknp/U5Mdb5Ol8Q5ZLp67UqyTbNFJJ3unVxZ8iX3g1fMN79S1JQ==
+ dependencies:
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.0.0"
+ crelt "^1.0.5"
+
+"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.4", "@codemirror/state@^6.2.0":
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.2.0.tgz#a0fb08403ced8c2a68d1d0acee926bd20be922f2"
+ integrity sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==
+
+"@codemirror/theme-one-dark@^6.1.2":
+ version "6.1.2"
+ resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz#fcef9f9cfc17a07836cb7da17c9f6d7231064df8"
+ integrity sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==
+ dependencies:
+ "@codemirror/language" "^6.0.0"
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.0.0"
+ "@lezer/highlight" "^1.0.0"
+
+"@codemirror/view@^6.0.0", "@codemirror/view@^6.11.2", "@codemirror/view@^6.6.0":
+ version "6.11.2"
+ resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.11.2.tgz#964a746119e6d07c75fddecaf90cb463ccf59f71"
+ integrity sha512-AzxJ9Aub6ubBvoPBGvjcd4zITqcBBiLpJ89z0ZjnphOHncbvUvQcb9/WMVGpuwTT95+jW4knkH6gFIy0oLdaUQ==
+ dependencies:
+ "@codemirror/state" "^6.1.4"
+ style-mod "^4.0.0"
+ w3c-keyname "^2.2.4"
+
"@colors/colors@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
@@ -3531,6 +3609,33 @@
validate-npm-package-name "^4.0.0"
yargs-parser "20.2.4"
+"@lezer/common@^1.0.0":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.0.2.tgz#8fb9b86bdaa2ece57e7d59e5ffbcb37d71815087"
+ integrity sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng==
+
+"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.1.4.tgz#98ed821e89f72981b7ba590474e6ee86c8185619"
+ integrity sha512-IECkFmw2l7sFcYXrV8iT9GeY4W0fU4CxX0WMwhmhMIVjoDdD1Hr6q3G2NqVtLg/yVe5n7i4menG3tJ2r4eCrPQ==
+ dependencies:
+ "@lezer/common" "^1.0.0"
+
+"@lezer/javascript@^1.0.0":
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.3.tgz#f59e764a0578184c6fb86abb5279a9679777c3ba"
+ integrity sha512-k7Eo9z9B1supZ5cCD4ilQv/RZVN30eUQL+gGbr6ybrEY3avBAL5MDiYi2aa23Aj0A79ry4rJRvPAwE2TM8bd+A==
+ dependencies:
+ "@lezer/highlight" "^1.1.3"
+ "@lezer/lr" "^1.3.0"
+
+"@lezer/lr@^1.0.0", "@lezer/lr@^1.3.0":
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.3.4.tgz#8795bf2ba4f69b998e8fb4b5a7c57ea68753474c"
+ integrity sha512-7o+e4og/QoC/6btozDPJqnzBhUaD1fMfmvnEKQO1wRRiTse1WxaJ3OMEXZJnkgT6HCcTVOctSoXK9jGJw2oe9g==
+ dependencies:
+ "@lezer/common" "^1.0.0"
+
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.10"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c"
@@ -9139,6 +9244,11 @@ create-require@^1.1.0:
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
+crelt@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.5.tgz#57c0d52af8c859e354bace1883eb2e1eb182bb94"
+ integrity sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==
+
cron-parser@^4.2.1:
version "4.7.1"
resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.7.1.tgz#1e325a6a18e797a634ada1e2599ece0b6b5ed177"
@@ -23688,6 +23798,11 @@ style-loader@^3.3.1:
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575"
integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==
+style-mod@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.0.3.tgz#136c4abc905f82a866a18b39df4dc08ec762b1ad"
+ integrity sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw==
+
stylehacks@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9"
@@ -25390,6 +25505,11 @@ w3c-hr-time@^1.0.2:
dependencies:
browser-process-hrtime "^1.0.0"
+w3c-keyname@^2.2.4:
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.6.tgz#8412046116bc16c5d73d4e612053ea10a189c85f"
+ integrity sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==
+
w3c-xmlserializer@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a"