Merge pull request #10732 from Budibase/feature/binding-v2-updates
Binding V2 Updates
This commit is contained in:
commit
8c144bbe2c
|
@ -56,6 +56,8 @@ export default function positionDropdown(element, opts) {
|
||||||
styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width
|
styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width
|
||||||
} else if (align === "right-outside") {
|
} else if (align === "right-outside") {
|
||||||
styles.left = anchorBounds.right + offset
|
styles.left = anchorBounds.right + offset
|
||||||
|
} else if (align === "left-outside") {
|
||||||
|
styles.left = anchorBounds.left - elementBounds.width - offset
|
||||||
} else {
|
} else {
|
||||||
styles.left = anchorBounds.left
|
styles.left = anchorBounds.left
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,13 @@
|
||||||
import Button from "../Button/Button.svelte"
|
import Button from "../Button/Button.svelte"
|
||||||
import Body from "../Typography/Body.svelte"
|
import Body from "../Typography/Body.svelte"
|
||||||
import Heading from "../Typography/Heading.svelte"
|
import Heading from "../Typography/Heading.svelte"
|
||||||
|
import { setContext } from "svelte"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let fillWidth
|
export let fillWidth
|
||||||
export let left = "314px"
|
export let left = "314px"
|
||||||
export let width = "calc(100% - 626px)"
|
export let width = "calc(100% - 626px)"
|
||||||
|
export let headless = false
|
||||||
|
|
||||||
let visible = false
|
let visible = false
|
||||||
|
|
||||||
|
@ -25,6 +27,11 @@
|
||||||
visible = false
|
visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setContext("drawer-actions", {
|
||||||
|
hide,
|
||||||
|
show,
|
||||||
|
})
|
||||||
|
|
||||||
const easeInOutQuad = x => {
|
const easeInOutQuad = x => {
|
||||||
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2
|
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2
|
||||||
}
|
}
|
||||||
|
@ -50,6 +57,7 @@
|
||||||
transition:slide|local
|
transition:slide|local
|
||||||
style={`width: ${width}; left: ${left};`}
|
style={`width: ${width}; left: ${left};`}
|
||||||
>
|
>
|
||||||
|
{#if !headless}
|
||||||
<header>
|
<header>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<Heading size="XS">{title}</Heading>
|
<Heading size="XS">{title}</Heading>
|
||||||
|
@ -62,6 +70,7 @@
|
||||||
<slot name="buttons" />
|
<slot name="buttons" />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
{/if}
|
||||||
<slot name="body" />
|
<slot name="body" />
|
||||||
</section>
|
</section>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
export let emphasized = false
|
export let emphasized = false
|
||||||
export let onTop = false
|
export let onTop = false
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
|
export let beforeSwitch = null
|
||||||
|
|
||||||
let thisSelected = undefined
|
let thisSelected = undefined
|
||||||
|
|
||||||
|
@ -28,10 +29,15 @@
|
||||||
thisSelected = selected
|
thisSelected = selected
|
||||||
dispatch("select", thisSelected)
|
dispatch("select", thisSelected)
|
||||||
} else if ($tab.title !== thisSelected) {
|
} else if ($tab.title !== thisSelected) {
|
||||||
|
if (typeof beforeSwitch == "function") {
|
||||||
|
const proceed = beforeSwitch($tab.title)
|
||||||
|
if (proceed) {
|
||||||
thisSelected = $tab.title
|
thisSelected = $tab.title
|
||||||
selected = $tab.title
|
selected = $tab.title
|
||||||
dispatch("select", thisSelected)
|
dispatch("select", thisSelected)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($tab.title !== thisSelected) {
|
if ($tab.title !== thisSelected) {
|
||||||
tab.update(state => {
|
tab.update(state => {
|
||||||
state.title = thisSelected
|
state.title = thisSelected
|
||||||
|
|
|
@ -63,6 +63,13 @@
|
||||||
"@budibase/shared-core": "0.0.0",
|
"@budibase/shared-core": "0.0.0",
|
||||||
"@budibase/string-templates": "0.0.0",
|
"@budibase/string-templates": "0.0.0",
|
||||||
"@budibase/types": "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/fontawesome-svg-core": "^6.2.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||||
|
|
|
@ -77,7 +77,7 @@ export const getAuthBindings = () => {
|
||||||
runtime: `${safeUser}.${safeOAuth2}.${safeAccessToken}`,
|
runtime: `${safeUser}.${safeOAuth2}.${safeAccessToken}`,
|
||||||
readable: `Current User.OAuthToken`,
|
readable: `Current User.OAuthToken`,
|
||||||
key: "accessToken",
|
key: "accessToken",
|
||||||
display: { name: "OAuthToken" },
|
display: { name: "OAuthToken", type: "text" },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -434,6 +434,9 @@ export const getUserBindings = () => {
|
||||||
providerId: "user",
|
providerId: "user",
|
||||||
category: "Current User",
|
category: "Current User",
|
||||||
icon: "User",
|
icon: "User",
|
||||||
|
display: {
|
||||||
|
name: key,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -550,7 +553,7 @@ const getUrlBindings = asset => {
|
||||||
readableBinding: `URL.${param}`,
|
readableBinding: `URL.${param}`,
|
||||||
category: "URL",
|
category: "URL",
|
||||||
icon: "RailTop",
|
icon: "RailTop",
|
||||||
display: { type: "string" },
|
display: { type: "string", name: param },
|
||||||
}))
|
}))
|
||||||
const queryParamsBinding = {
|
const queryParamsBinding = {
|
||||||
type: "context",
|
type: "context",
|
||||||
|
@ -558,7 +561,7 @@ const getUrlBindings = asset => {
|
||||||
readableBinding: "Query params",
|
readableBinding: "Query params",
|
||||||
category: "URL",
|
category: "URL",
|
||||||
icon: "RailTop",
|
icon: "RailTop",
|
||||||
display: { type: "object" },
|
display: { type: "object", name: "Query params" },
|
||||||
}
|
}
|
||||||
return urlParamBindings.concat([queryParamsBinding])
|
return urlParamBindings.concat([queryParamsBinding])
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,7 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
customTheme: {},
|
customTheme: {},
|
||||||
previewDevice: "desktop",
|
previewDevice: "desktop",
|
||||||
highlightedSettingKey: null,
|
highlightedSettingKey: null,
|
||||||
|
propertyFocus: null,
|
||||||
builderSidePanel: false,
|
builderSidePanel: false,
|
||||||
hasLock: true,
|
hasLock: true,
|
||||||
|
|
||||||
|
@ -1326,6 +1327,12 @@ export const getFrontendStore = () => {
|
||||||
highlightedSettingKey: key,
|
highlightedSettingKey: key,
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
propertyFocus: key => {
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
propertyFocus: key,
|
||||||
|
}))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dnd: {
|
dnd: {
|
||||||
start: component => {
|
start: component => {
|
||||||
|
|
|
@ -29,7 +29,10 @@
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
||||||
import { LuceneUtils } from "@budibase/frontend-core"
|
import { LuceneUtils } from "@budibase/frontend-core"
|
||||||
import { getSchemaForTable } from "builderStore/dataBinding"
|
import {
|
||||||
|
getSchemaForTable,
|
||||||
|
getEnvironmentBindings,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
@ -210,6 +213,19 @@
|
||||||
}
|
}
|
||||||
const outputs = Object.entries(schema)
|
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(
|
bindings = bindings.concat(
|
||||||
outputs.map(([name, value]) => {
|
outputs.map(([name, value]) => {
|
||||||
let runtimeName = isLoopBlock
|
let runtimeName = isLoopBlock
|
||||||
|
@ -218,17 +234,24 @@
|
||||||
? `steps[${idx - loopBlockCount}].${name}`
|
? `steps[${idx - loopBlockCount}].${name}`
|
||||||
: `steps.${idx - loopBlockCount}.${name}`
|
: `steps.${idx - loopBlockCount}.${name}`
|
||||||
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
|
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
|
||||||
return {
|
const categoryName =
|
||||||
label: runtime,
|
|
||||||
type: value.type,
|
|
||||||
description: value.description,
|
|
||||||
category:
|
|
||||||
idx === 0
|
idx === 0
|
||||||
? "Trigger outputs"
|
? "Trigger outputs"
|
||||||
: isLoopBlock
|
: isLoopBlock
|
||||||
? "Loop Outputs"
|
? "Loop Outputs"
|
||||||
: `Step ${idx - loopBlockCount} outputs`,
|
: `Step ${idx - loopBlockCount} outputs`
|
||||||
path: runtime,
|
return {
|
||||||
|
readableBinding: runtime,
|
||||||
|
runtimeBinding: runtime,
|
||||||
|
type: value.type,
|
||||||
|
description: value.description,
|
||||||
|
icon: bindingIcon,
|
||||||
|
category: categoryName,
|
||||||
|
display: {
|
||||||
|
type: value.type,
|
||||||
|
name: name,
|
||||||
|
rank: bindindingRank,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -237,15 +260,12 @@
|
||||||
// Environment bindings
|
// Environment bindings
|
||||||
if ($licensing.environmentVariablesEnabled) {
|
if ($licensing.environmentVariablesEnabled) {
|
||||||
bindings = bindings.concat(
|
bindings = bindings.concat(
|
||||||
$environment.variables.map(variable => {
|
getEnvironmentBindings().map(binding => {
|
||||||
return {
|
return {
|
||||||
label: `env.${variable.name}`,
|
...binding,
|
||||||
path: `env.${variable.name}`,
|
|
||||||
icon: "Key",
|
|
||||||
category: "Environment",
|
|
||||||
display: {
|
display: {
|
||||||
type: "string",
|
...binding.display,
|
||||||
name: variable.name,
|
rank: 98,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,287 @@
|
||||||
|
<script>
|
||||||
|
import { Label } from "@budibase/bbui"
|
||||||
|
import { onMount, createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
import {
|
||||||
|
autocompletion,
|
||||||
|
closeBrackets,
|
||||||
|
completionKeymap,
|
||||||
|
closeBracketsKeymap,
|
||||||
|
} from "@codemirror/autocomplete"
|
||||||
|
import {
|
||||||
|
EditorView,
|
||||||
|
lineNumbers,
|
||||||
|
keymap,
|
||||||
|
highlightSpecialChars,
|
||||||
|
drawSelection,
|
||||||
|
dropCursor,
|
||||||
|
highlightActiveLine,
|
||||||
|
highlightActiveLineGutter,
|
||||||
|
highlightWhitespace,
|
||||||
|
placeholder as placeholderFn,
|
||||||
|
MatchDecorator,
|
||||||
|
ViewPlugin,
|
||||||
|
Decoration,
|
||||||
|
} from "@codemirror/view"
|
||||||
|
import {
|
||||||
|
bracketMatching,
|
||||||
|
foldKeymap,
|
||||||
|
foldGutter,
|
||||||
|
syntaxHighlighting,
|
||||||
|
} from "@codemirror/language"
|
||||||
|
import { oneDark, oneDarkHighlightStyle } from "@codemirror/theme-one-dark"
|
||||||
|
import {
|
||||||
|
defaultKeymap,
|
||||||
|
historyKeymap,
|
||||||
|
history,
|
||||||
|
indentWithTab,
|
||||||
|
} from "@codemirror/commands"
|
||||||
|
import { Compartment } from "@codemirror/state"
|
||||||
|
import { javascript } from "@codemirror/lang-javascript"
|
||||||
|
import { EditorModes, getDefaultTheme } from "./"
|
||||||
|
import { themeStore } from "builderStore"
|
||||||
|
|
||||||
|
export let label
|
||||||
|
export let completions = []
|
||||||
|
export let height = 200
|
||||||
|
export let resize = "none"
|
||||||
|
export let mode = EditorModes.Handlebars
|
||||||
|
export let value = ""
|
||||||
|
export let placeholder = null
|
||||||
|
|
||||||
|
// Export a function to expose caret position
|
||||||
|
export const getCaretPosition = () => {
|
||||||
|
const selection_range = editor.state.selection.ranges[0]
|
||||||
|
return {
|
||||||
|
start: selection_range.from,
|
||||||
|
end: selection_range.to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const insertAtPos = opts => {
|
||||||
|
// Updating the value inside.
|
||||||
|
// Retain focus
|
||||||
|
editor.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: opts.start || editor.state.doc.length,
|
||||||
|
to: opts.end || editor.state.doc.length,
|
||||||
|
insert: opts.value,
|
||||||
|
},
|
||||||
|
selection: opts.cursor
|
||||||
|
? {
|
||||||
|
anchor: opts.start + opts.value.length,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// For handlebars only.
|
||||||
|
const bindStyle = new MatchDecorator({
|
||||||
|
regexp: /{{[."#\-\w\s\][]*}}/g,
|
||||||
|
decoration: () => {
|
||||||
|
return Decoration.mark({
|
||||||
|
tag: "span",
|
||||||
|
attributes: {
|
||||||
|
class: "binding-wrap",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let plugin = ViewPlugin.define(
|
||||||
|
view => ({
|
||||||
|
decorations: bindStyle.createDeco(view),
|
||||||
|
update(u) {
|
||||||
|
this.decorations = bindStyle.updateDeco(u, this.decorations)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
decorations: v => v.decorations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
// Theming!
|
||||||
|
let currentTheme = $themeStore?.theme
|
||||||
|
let isDark = !currentTheme.includes("light")
|
||||||
|
let themeConfig = new Compartment()
|
||||||
|
|
||||||
|
const buildKeymap = () => {
|
||||||
|
const baseMap = [
|
||||||
|
...closeBracketsKeymap,
|
||||||
|
...defaultKeymap,
|
||||||
|
...historyKeymap,
|
||||||
|
...foldKeymap,
|
||||||
|
...completionKeymap,
|
||||||
|
indentWithTab,
|
||||||
|
]
|
||||||
|
return baseMap
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildBaseExtensions = () => {
|
||||||
|
return [
|
||||||
|
...(mode.name === "handlebars" ? [plugin] : []),
|
||||||
|
history(),
|
||||||
|
drawSelection(),
|
||||||
|
dropCursor(),
|
||||||
|
bracketMatching(),
|
||||||
|
closeBrackets(),
|
||||||
|
highlightActiveLine(),
|
||||||
|
syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }),
|
||||||
|
highlightActiveLineGutter(),
|
||||||
|
highlightSpecialChars(),
|
||||||
|
autocompletion({
|
||||||
|
override: [...completions],
|
||||||
|
closeOnBlur: true,
|
||||||
|
icons: false,
|
||||||
|
optionClass: () => "autocomplete-option",
|
||||||
|
}),
|
||||||
|
EditorView.lineWrapping,
|
||||||
|
EditorView.updateListener.of(v => {
|
||||||
|
const docStr = v.state.doc?.toString()
|
||||||
|
if (docStr === value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dispatch("change", docStr)
|
||||||
|
}),
|
||||||
|
keymap.of(buildKeymap()),
|
||||||
|
themeConfig.of([
|
||||||
|
getDefaultTheme({
|
||||||
|
height: editorHeight,
|
||||||
|
resize,
|
||||||
|
dark: isDark,
|
||||||
|
}),
|
||||||
|
...(isDark ? [oneDark] : []),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildExtensions = base => {
|
||||||
|
const complete = [...base]
|
||||||
|
if (mode.name == "javascript") {
|
||||||
|
complete.push(javascript())
|
||||||
|
complete.push(highlightWhitespace())
|
||||||
|
complete.push(lineNumbers())
|
||||||
|
complete.push(foldGutter())
|
||||||
|
complete.push(
|
||||||
|
EditorView.inputHandler.of((view, from, to, insert) => {
|
||||||
|
if (insert === "$") {
|
||||||
|
let { text } = view.state.doc.lineAt(from)
|
||||||
|
|
||||||
|
const left = from ? text.substring(0, from) : ""
|
||||||
|
const right = to ? text.substring(to) : ""
|
||||||
|
const wrap = !left.includes('$("') || !right.includes('")')
|
||||||
|
const tr = view.state.update(
|
||||||
|
{
|
||||||
|
changes: [{ from, insert: wrap ? '$("")' : "$" }],
|
||||||
|
selection: {
|
||||||
|
anchor: from + (wrap ? 3 : 1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scrollIntoView: true,
|
||||||
|
userEvent: "input.type",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
view.dispatch(tr)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeholder) {
|
||||||
|
complete.push(placeholderFn(placeholder))
|
||||||
|
}
|
||||||
|
return complete
|
||||||
|
}
|
||||||
|
|
||||||
|
let textarea
|
||||||
|
let editor
|
||||||
|
let mounted = false
|
||||||
|
let isEditorInitialised = false
|
||||||
|
|
||||||
|
const initEditor = () => {
|
||||||
|
const baseExtensions = buildBaseExtensions()
|
||||||
|
|
||||||
|
editor = new EditorView({
|
||||||
|
doc: value,
|
||||||
|
extensions: buildExtensions(baseExtensions),
|
||||||
|
parent: textarea,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$: editorHeight = typeof height === "number" ? `${height}px` : height
|
||||||
|
|
||||||
|
// Init when all elements are ready
|
||||||
|
$: if (mounted && !isEditorInitialised) {
|
||||||
|
isEditorInitialised = true
|
||||||
|
initEditor()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme change
|
||||||
|
$: if (mounted && isEditorInitialised && $themeStore?.theme) {
|
||||||
|
if (currentTheme != $themeStore?.theme) {
|
||||||
|
currentTheme = $themeStore?.theme
|
||||||
|
isDark = !currentTheme.includes("light")
|
||||||
|
|
||||||
|
// Issue theme compartment update
|
||||||
|
editor.dispatch({
|
||||||
|
effects: themeConfig.reconfigure([
|
||||||
|
getDefaultTheme({
|
||||||
|
height: editorHeight,
|
||||||
|
resize,
|
||||||
|
dark: isDark,
|
||||||
|
}),
|
||||||
|
...(isDark ? [oneDark] : []),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
mounted = true
|
||||||
|
return () => {
|
||||||
|
if (editor) {
|
||||||
|
editor.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if label}
|
||||||
|
<div>
|
||||||
|
<Label small>{label}</Label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="code-editor">
|
||||||
|
<div tabindex="-1" bind:this={textarea} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Push into theme */
|
||||||
|
.code-editor :global(.cm-tooltip.cm-completionInfo) {
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.code-editor :global(.cm-tooltip-autocomplete > ul > li[aria-selected]) {
|
||||||
|
border-radius: var(
|
||||||
|
--spectrum-popover-border-radius,
|
||||||
|
var(--spectrum-alias-border-radius-regular)
|
||||||
|
),
|
||||||
|
var(
|
||||||
|
--spectrum-popover-border-radius,
|
||||||
|
var(--spectrum-alias-border-radius-regular)
|
||||||
|
),
|
||||||
|
0, 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-editor :global(.autocomplete-option .cm-completionDetail) {
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -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
|
||||||
|
? `<div class="binding__example">${helper.example}</div>`
|
||||||
|
: ""
|
||||||
|
const descriptionMarkup = sanitizeHtml(helper.description, {
|
||||||
|
allowedTags: [],
|
||||||
|
allowedAttributes: {},
|
||||||
|
})
|
||||||
|
const descriptionNodeHtml = `<div class="binding__description">${descriptionMarkup}</div>`
|
||||||
|
|
||||||
|
ele.innerHTML = `
|
||||||
|
${exampleNodeHtml}
|
||||||
|
${descriptionNodeHtml}
|
||||||
|
`
|
||||||
|
return ele
|
||||||
|
}
|
||||||
|
|
||||||
|
const toSpectrumIcon = name => {
|
||||||
|
return `<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeM"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="false"
|
||||||
|
aria-label="${name}-section-icon"
|
||||||
|
>
|
||||||
|
<use style="pointer-events: none;" xlink:href="#spectrum-icon-18-${name}" />
|
||||||
|
</svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
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)}<span>${sectionName}</span>`
|
||||||
|
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
|
||||||
|
? `<div class="binding__example">{{ ${binding.readableBinding} }}</div>`
|
||||||
|
: ""
|
||||||
|
|
||||||
|
const descriptionNodeHtml = binding.description
|
||||||
|
? `<div class="binding__description">${binding.description}</div>`
|
||||||
|
: ""
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -1,17 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import groupBy from "lodash/fp/groupBy"
|
|
||||||
import {
|
import {
|
||||||
Search,
|
|
||||||
TextArea,
|
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
Body,
|
Body,
|
||||||
Layout,
|
|
||||||
Button,
|
Button,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
|
Heading,
|
||||||
Icon,
|
Icon,
|
||||||
Popover,
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
import {
|
import {
|
||||||
|
@ -23,11 +19,21 @@
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { handlebarsCompletions } from "constants/completions"
|
import { store } from "builderStore"
|
||||||
import { addHBSBinding, addJSBinding } from "./utils"
|
|
||||||
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
|
||||||
import { convertToJS } from "@budibase/string-templates"
|
import { convertToJS } from "@budibase/string-templates"
|
||||||
import { admin } from "stores/portal"
|
import { admin } from "stores/portal"
|
||||||
|
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
||||||
|
import {
|
||||||
|
getHelperCompletions,
|
||||||
|
jsAutocomplete,
|
||||||
|
hbAutocomplete,
|
||||||
|
EditorModes,
|
||||||
|
bindingsToCompletions,
|
||||||
|
hbInsert,
|
||||||
|
jsInsert,
|
||||||
|
} from "../CodeEditor"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import BindingPicker from "./BindingPicker.svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -41,54 +47,21 @@
|
||||||
export let allowJS = false
|
export let allowJS = false
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
|
|
||||||
let helpers = handlebarsCompletions()
|
const drawerActions = getContext("drawer-actions")
|
||||||
|
const bindingDrawerActions = getContext("binding-drawer-actions")
|
||||||
|
|
||||||
let getCaretPosition
|
let getCaretPosition
|
||||||
let search = ""
|
let insertAtPos
|
||||||
let initialValueJS = typeof value === "string" && value?.startsWith("{{ js ")
|
let initialValueJS = typeof value === "string" && value?.startsWith("{{ js ")
|
||||||
let mode = initialValueJS ? "JavaScript" : "Handlebars"
|
let mode = initialValueJS ? "JavaScript" : "Text"
|
||||||
let jsValue = initialValueJS ? value : null
|
let jsValue = initialValueJS ? value : null
|
||||||
let hbsValue = initialValueJS ? null : value
|
let hbsValue = initialValueJS ? null : value
|
||||||
|
let sidebar = true
|
||||||
let selectedCategory = null
|
let targetMode = null
|
||||||
|
|
||||||
let popover
|
|
||||||
let popoverAnchor
|
|
||||||
let hoverTarget
|
|
||||||
|
|
||||||
$: usingJS = mode === "JavaScript"
|
$: usingJS = mode === "JavaScript"
|
||||||
$: searchRgx = new RegExp(search, "ig")
|
$: editorMode = mode == "JavaScript" ? EditorModes.JS : EditorModes.Handlebars
|
||||||
$: categories = Object.entries(groupBy("category", bindings))
|
$: bindingCompletions = bindingsToCompletions(bindings, editorMode)
|
||||||
|
|
||||||
$: bindingIcons = bindings?.reduce((acc, ele) => {
|
|
||||||
if (ele.icon) {
|
|
||||||
acc[ele.category] = acc[ele.category] || ele.icon
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
$: categoryIcons = { ...bindingIcons, Helpers: "MagicWand" }
|
|
||||||
|
|
||||||
$: filteredCategories = categories
|
|
||||||
.map(([name, categoryBindings]) => ({
|
|
||||||
name,
|
|
||||||
bindings: categoryBindings?.filter(binding => {
|
|
||||||
return binding.readableBinding.match(searchRgx)
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
.filter(category => {
|
|
||||||
return (
|
|
||||||
category.bindings?.length > 0 &&
|
|
||||||
(!selectedCategory ? true : selectedCategory === category.name)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
$: filteredHelpers = helpers?.filter(helper => {
|
|
||||||
return helper.label.match(searchRgx) || helper.description.match(searchRgx)
|
|
||||||
})
|
|
||||||
|
|
||||||
$: categoryNames = getCategoryNames(categories)
|
|
||||||
|
|
||||||
$: codeMirrorHints = bindings?.map(x => `$("${x.readableBinding}")`)
|
|
||||||
|
|
||||||
const updateValue = val => {
|
const updateValue = val => {
|
||||||
valid = isValid(readableToRuntimeBinding(bindings, val))
|
valid = isValid(readableToRuntimeBinding(bindings, val))
|
||||||
|
@ -97,43 +70,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCategoryNames = categories => {
|
|
||||||
let names = [...categories.map(cat => cat[0])]
|
|
||||||
if (allowHelpers) {
|
|
||||||
names.push("Helpers")
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds a JS/HBS helper to the expression
|
// Adds a JS/HBS helper to the expression
|
||||||
const addHelper = (helper, js) => {
|
const onSelectHelper = (helper, js) => {
|
||||||
let tempVal
|
|
||||||
const pos = getCaretPosition()
|
const pos = getCaretPosition()
|
||||||
|
const { start, end } = pos
|
||||||
if (js) {
|
if (js) {
|
||||||
const decoded = decodeJSBinding(jsValue)
|
let js = decodeJSBinding(jsValue)
|
||||||
tempVal = jsValue = encodeJSBinding(
|
const insertVal = jsInsert(js, start, end, helper.text, { helper: true })
|
||||||
addJSBinding(decoded, pos, helper.text, { helper: true })
|
insertAtPos({ start, end, value: insertVal })
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
tempVal = hbsValue = addHBSBinding(hbsValue, pos, helper.text)
|
const insertVal = hbInsert(hbsValue, start, end, helper.text)
|
||||||
|
insertAtPos({ start, end, value: insertVal })
|
||||||
}
|
}
|
||||||
updateValue(tempVal)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a data binding to the expression
|
// Adds a data binding to the expression
|
||||||
const addBinding = (binding, { forceJS } = {}) => {
|
const onSelectBinding = (binding, { forceJS } = {}) => {
|
||||||
|
const { start, end } = getCaretPosition()
|
||||||
if (usingJS || forceJS) {
|
if (usingJS || forceJS) {
|
||||||
let js = decodeJSBinding(jsValue)
|
let js = decodeJSBinding(jsValue)
|
||||||
js = addJSBinding(js, getCaretPosition(), binding.readableBinding)
|
const insertVal = jsInsert(js, start, end, binding.readableBinding)
|
||||||
jsValue = encodeJSBinding(js)
|
insertAtPos({ start, end, value: insertVal })
|
||||||
updateValue(jsValue)
|
|
||||||
} else {
|
} else {
|
||||||
hbsValue = addHBSBinding(
|
const insertVal = hbInsert(hbsValue, start, end, binding.readableBinding)
|
||||||
hbsValue,
|
insertAtPos({ start, end, value: insertVal })
|
||||||
getCaretPosition(),
|
|
||||||
binding.readableBinding
|
|
||||||
)
|
|
||||||
updateValue(hbsValue)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,24 +112,25 @@
|
||||||
updateValue(jsValue)
|
updateValue(jsValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const switchMode = () => {
|
||||||
|
if (targetMode == "Text") {
|
||||||
|
jsValue = null
|
||||||
|
updateValue(jsValue)
|
||||||
|
} else {
|
||||||
|
hbsValue = null
|
||||||
|
updateValue(hbsValue)
|
||||||
|
}
|
||||||
|
mode = targetMode + ""
|
||||||
|
targetMode = null
|
||||||
|
}
|
||||||
|
|
||||||
const convert = () => {
|
const convert = () => {
|
||||||
const runtime = readableToRuntimeBinding(bindings, hbsValue)
|
const runtime = readableToRuntimeBinding(bindings, hbsValue)
|
||||||
const runtimeJs = encodeJSBinding(convertToJS(runtime))
|
const runtimeJs = encodeJSBinding(convertToJS(runtime))
|
||||||
jsValue = runtimeToReadableBinding(bindings, runtimeJs)
|
jsValue = runtimeToReadableBinding(bindings, runtimeJs)
|
||||||
hbsValue = null
|
hbsValue = null
|
||||||
mode = "JavaScript"
|
mode = "JavaScript"
|
||||||
addBinding("", { forceJS: true })
|
onSelectBinding("", { forceJS: true })
|
||||||
}
|
|
||||||
|
|
||||||
const getHelperExample = (helper, js) => {
|
|
||||||
let example = helper.example || ""
|
|
||||||
if (js) {
|
|
||||||
example = convertToJS(example).split("\n")[0].split("= ")[1]
|
|
||||||
if (example === "null;") {
|
|
||||||
example = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return example || ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
@ -177,241 +138,250 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="detailPopover">
|
<span class="binding-drawer">
|
||||||
<Popover
|
|
||||||
align="right-outside"
|
|
||||||
bind:this={popover}
|
|
||||||
anchor={popoverAnchor}
|
|
||||||
maxWidth={300}
|
|
||||||
dismissible={false}
|
|
||||||
>
|
|
||||||
<Layout gap="S">
|
|
||||||
<div class="helper">
|
|
||||||
{#if hoverTarget.title}
|
|
||||||
<div class="helper__name">{hoverTarget.title}</div>
|
|
||||||
{/if}
|
|
||||||
{#if hoverTarget.description}
|
|
||||||
<div class="helper__description">
|
|
||||||
{@html hoverTarget.description}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if hoverTarget.example}
|
|
||||||
<pre class="helper__example">{hoverTarget.example}</pre>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</Popover>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<svelte:fragment slot="sidebar">
|
|
||||||
<Layout noPadding gap="S">
|
|
||||||
{#if selectedCategory}
|
|
||||||
<div>
|
|
||||||
<ActionButton
|
|
||||||
secondary
|
|
||||||
icon={"ArrowLeft"}
|
|
||||||
on:click={() => {
|
|
||||||
selectedCategory = null
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !selectedCategory}
|
|
||||||
<div class="heading">Search</div>
|
|
||||||
<Search placeholder="Search" bind:value={search} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !selectedCategory && !search}
|
|
||||||
<ul class="category-list">
|
|
||||||
{#each categoryNames as categoryName}
|
|
||||||
<li
|
|
||||||
on:click={() => {
|
|
||||||
selectedCategory = categoryName
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name={categoryIcons[categoryName]} />
|
|
||||||
<span class="category-name">{categoryName} </span>
|
|
||||||
<span class="category-chevron"><Icon name="ChevronRight" /></span>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if selectedCategory || search}
|
|
||||||
{#each filteredCategories as category}
|
|
||||||
{#if category.bindings?.length}
|
|
||||||
<div class="cat-heading">
|
|
||||||
<Icon name={categoryIcons[category.name]} />{category.name}
|
|
||||||
</div>
|
|
||||||
<ul>
|
|
||||||
{#each category.bindings as binding}
|
|
||||||
<li
|
|
||||||
class="binding"
|
|
||||||
on:mouseenter={e => {
|
|
||||||
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)}
|
|
||||||
>
|
|
||||||
<span class="binding__label">
|
|
||||||
{#if binding.display?.name}
|
|
||||||
{binding.display.name}
|
|
||||||
{:else if binding.fieldSchema?.name}
|
|
||||||
{binding.fieldSchema?.name}
|
|
||||||
{:else}
|
|
||||||
{binding.readableBinding}
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{#if binding.display?.type || binding.fieldSchema?.type}
|
|
||||||
<span class="binding__typeWrap">
|
|
||||||
<span class="binding__type">
|
|
||||||
{binding.display?.type || binding.fieldSchema?.type}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#if selectedCategory === "Helpers" || search}
|
|
||||||
{#if filteredHelpers?.length}
|
|
||||||
<div class="heading">Helpers</div>
|
|
||||||
<ul class="helpers">
|
|
||||||
{#each filteredHelpers as helper}
|
|
||||||
<li
|
|
||||||
class="binding"
|
|
||||||
on:click={() => addHelper(helper, usingJS)}
|
|
||||||
on:mouseenter={e => {
|
|
||||||
popoverAnchor = e.target
|
|
||||||
if (!helper.displayText && helper.description) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hoverTarget = {
|
|
||||||
title: helper.displayText,
|
|
||||||
description: helper.description,
|
|
||||||
example: getHelperExample(helper, usingJS),
|
|
||||||
}
|
|
||||||
popover.show()
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
on:mouseleave={() => {
|
|
||||||
popover.hide()
|
|
||||||
popoverAnchor = null
|
|
||||||
hoverTarget = null
|
|
||||||
}}
|
|
||||||
on:focus={() => {}}
|
|
||||||
on:blur={() => {}}
|
|
||||||
>
|
|
||||||
<span class="binding__label">{helper.displayText}</span>
|
|
||||||
<span class="binding__typeWrap">
|
|
||||||
<span class="binding__type">function</span>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
</svelte:fragment>
|
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<Tabs selected={mode} on:select={onChangeMode}>
|
<Tabs
|
||||||
<Tab title="Handlebars">
|
selected={mode}
|
||||||
<div class="main-content">
|
on:select={onChangeMode}
|
||||||
<TextArea
|
beforeSwitch={selectedMode => {
|
||||||
bind:getCaretPosition
|
if (selectedMode == mode) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get the current mode value
|
||||||
|
const editorValue = usingJS ? decodeJSBinding(jsValue) : hbsValue
|
||||||
|
|
||||||
|
if (editorValue) {
|
||||||
|
targetMode = selectedMode
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab title="Text">
|
||||||
|
<div class="main-content" class:binding-panel={sidebar}>
|
||||||
|
<div class="editor">
|
||||||
|
<div class="overlay-wrap">
|
||||||
|
{#if targetMode}
|
||||||
|
<div class="mode-overlay">
|
||||||
|
<div class="prompt-body">
|
||||||
|
<Heading size="S">
|
||||||
|
{`Switch to ${targetMode}?`}
|
||||||
|
</Heading>
|
||||||
|
<Body>This will discard anything in your binding</Body>
|
||||||
|
<div class="switch-actions">
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
size="S"
|
||||||
|
on:click={() => {
|
||||||
|
targetMode = null
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No - keep text
|
||||||
|
</Button>
|
||||||
|
<Button cta size="S" on:click={switchMode}>
|
||||||
|
Yes - discard text
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<CodeEditor
|
||||||
value={hbsValue}
|
value={hbsValue}
|
||||||
on:change={onChangeHBSValue}
|
on:change={onChangeHBSValue}
|
||||||
placeholder="Add text, or click the objects on the left to add them to the textbox."
|
bind:getCaretPosition
|
||||||
|
bind:insertAtPos
|
||||||
|
completions={[
|
||||||
|
hbAutocomplete([
|
||||||
|
...bindingCompletions,
|
||||||
|
...getHelperCompletions(editorMode),
|
||||||
|
]),
|
||||||
|
]}
|
||||||
|
placeholder=""
|
||||||
|
height="100%"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="binding-footer">
|
||||||
|
<div class="messaging">
|
||||||
{#if !valid}
|
{#if !valid}
|
||||||
<p class="syntax-error">
|
<div class="syntax-error">
|
||||||
Current Handlebars syntax is invalid, please check the guide
|
Current Handlebars syntax is invalid, please check the
|
||||||
|
guide
|
||||||
<a href="https://handlebarsjs.com/guide/">here</a>
|
<a href="https://handlebarsjs.com/guide/">here</a>
|
||||||
for more details.
|
for more details.
|
||||||
</p>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Icon name="FlashOn" />
|
||||||
|
<div class="messaging-wrap">
|
||||||
|
<div>
|
||||||
|
Add available bindings by typing {{ or use the
|
||||||
|
menu on the right
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
{#if $admin.isDev && allowJS}
|
{#if $admin.isDev && allowJS}
|
||||||
<div class="convert">
|
<ActionButton
|
||||||
<Button secondary on:click={convert}>Convert to JS</Button>
|
secondary
|
||||||
|
on:click={() => {
|
||||||
|
convert()
|
||||||
|
targetMode = null
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Convert To JS
|
||||||
|
</ActionButton>
|
||||||
|
{/if}
|
||||||
|
<ActionButton
|
||||||
|
secondary
|
||||||
|
icon={sidebar ? "RailRightClose" : "RailRightOpen"}
|
||||||
|
on:click={() => {
|
||||||
|
sidebar = !sidebar
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if sidebar}
|
||||||
|
<div class="binding-picker">
|
||||||
|
<BindingPicker
|
||||||
|
{bindings}
|
||||||
|
{allowHelpers}
|
||||||
|
addHelper={onSelectHelper}
|
||||||
|
addBinding={onSelectBinding}
|
||||||
|
mode={editorMode}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
{#if allowJS}
|
{#if allowJS}
|
||||||
<Tab title="JavaScript">
|
<Tab title="JavaScript">
|
||||||
<div class="main-content">
|
<div class="main-content" class:binding-panel={sidebar}>
|
||||||
<Layout noPadding gap="XS">
|
<div class="editor">
|
||||||
<CodeMirrorEditor
|
<div class="overlay-wrap">
|
||||||
bind:getCaretPosition
|
{#if targetMode}
|
||||||
height={200}
|
<div class="mode-overlay">
|
||||||
|
<div class="prompt-body">
|
||||||
|
<Heading size="S">
|
||||||
|
{`Switch to ${targetMode}?`}
|
||||||
|
</Heading>
|
||||||
|
<Body>This will discard anything in your binding</Body>
|
||||||
|
<div class="switch-actions">
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
size="S"
|
||||||
|
on:click={() => {
|
||||||
|
targetMode = null
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No - keep javascript
|
||||||
|
</Button>
|
||||||
|
<Button cta size="S" on:click={switchMode}>
|
||||||
|
Yes - discard javascript
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<CodeEditor
|
||||||
value={decodeJSBinding(jsValue)}
|
value={decodeJSBinding(jsValue)}
|
||||||
on:change={onChangeJSValue}
|
on:change={onChangeJSValue}
|
||||||
hints={codeMirrorHints}
|
completions={[
|
||||||
|
jsAutocomplete([
|
||||||
|
...bindingCompletions,
|
||||||
|
...getHelperCompletions(editorMode),
|
||||||
|
]),
|
||||||
|
]}
|
||||||
|
mode={EditorModes.JS}
|
||||||
|
bind:getCaretPosition
|
||||||
|
bind:insertAtPos
|
||||||
|
height="100%"
|
||||||
/>
|
/>
|
||||||
<Body size="S">
|
</div>
|
||||||
JavaScript expressions are executed as functions, so ensure that
|
<div class="binding-footer">
|
||||||
your expression returns a value.
|
<div class="messaging">
|
||||||
</Body>
|
<Icon name="FlashOn" />
|
||||||
</Layout>
|
<div class="messaging-wrap">
|
||||||
|
<div>
|
||||||
|
Add available bindings by typing $ or use the menu on
|
||||||
|
the right
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<ActionButton
|
||||||
|
secondary
|
||||||
|
icon={sidebar ? "RailRightClose" : "RailRightOpen"}
|
||||||
|
on:click={() => {
|
||||||
|
sidebar = !sidebar
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if sidebar}
|
||||||
|
<div class="binding-picker">
|
||||||
|
<BindingPicker
|
||||||
|
{bindings}
|
||||||
|
{allowHelpers}
|
||||||
|
addHelper={onSelectHelper}
|
||||||
|
addBinding={onSelectBinding}
|
||||||
|
mode={editorMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="drawer-actions">
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
quiet
|
||||||
|
on:click={() => {
|
||||||
|
store.actions.settings.propertyFocus(null)
|
||||||
|
drawerActions.hide()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
disabled={!valid}
|
||||||
|
on:click={() => {
|
||||||
|
bindingDrawerActions.save()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
|
</span>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
ul.helpers li * {
|
.messaging {
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
ul.category-list li {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
align-items: center;
|
min-width: 0;
|
||||||
}
|
|
||||||
ul.category-list .category-name {
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
ul.category-list .category-chevron {
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
ul.category-list .category-chevron :global(div.icon),
|
.messaging-wrap {
|
||||||
.cat-heading :global(div.icon) {
|
overflow: hidden;
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
li.binding {
|
.messaging-wrap > div {
|
||||||
display: flex;
|
text-overflow: ellipsis;
|
||||||
align-items: center;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
li.binding .binding__typeWrap {
|
.binding-drawer :global(.drawer-contents) {
|
||||||
flex: 1;
|
height: unset;
|
||||||
text-align: right;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
}
|
||||||
.main :global(textarea) {
|
.main :global(textarea) {
|
||||||
min-height: 202px !important;
|
min-height: 202px !important;
|
||||||
|
@ -423,86 +393,23 @@
|
||||||
padding: var(--spacing-s) var(--spacing-xl);
|
padding: var(--spacing-s) var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading,
|
.main :global(.spectrum-Tabs div.drawer-actions) {
|
||||||
.cat-heading {
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--spectrum-global-color-gray-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cat-heading {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
align-items: center;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
.main :global(.spectrum-Tabs-content),
|
||||||
list-style: none;
|
.main :global(.spectrum-Tabs-content .main-content) {
|
||||||
padding: 0;
|
margin-top: 0px;
|
||||||
margin: 0;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
.main :global(.spectrum-Tabs) {
|
||||||
font-size: var(--font-size-s);
|
|
||||||
padding: var(--spacing-m);
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
|
||||||
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
|
||||||
border-color 130ms ease-in-out;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
li:not(:last-of-type) {
|
|
||||||
margin-bottom: var(--spacing-s);
|
|
||||||
}
|
|
||||||
li :global(*) {
|
|
||||||
transition: color 130ms ease-in-out;
|
|
||||||
}
|
|
||||||
li:hover {
|
|
||||||
color: var(--spectrum-global-color-gray-900);
|
|
||||||
background-color: var(--spectrum-global-color-gray-50);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.binding__label {
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.binding__type {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
|
||||||
border-radius: var(--border-radius-s);
|
|
||||||
padding: 2px 4px;
|
|
||||||
margin-left: 2px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.helper {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
.helper__name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.helper__description,
|
|
||||||
.helper__description :global(*) {
|
|
||||||
color: var(--spectrum-global-color-gray-700);
|
|
||||||
}
|
|
||||||
.helper__example {
|
|
||||||
white-space: normal;
|
|
||||||
margin: 0.5rem 0 0 0;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.helper__description :global(p) {
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.syntax-error {
|
.syntax-error {
|
||||||
padding-top: var(--spacing-m);
|
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
@ -511,7 +418,65 @@
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.convert {
|
.binding-footer {
|
||||||
padding-top: var(--spacing-m);
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.main-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 315px;
|
||||||
|
}
|
||||||
|
.main-content.binding-panel {
|
||||||
|
grid-template-columns: 1fr 320px;
|
||||||
|
}
|
||||||
|
.binding-picker {
|
||||||
|
overflow-y: auto;
|
||||||
|
border-left: 2px solid var(--border-light);
|
||||||
|
border-left: var(--border-light);
|
||||||
|
}
|
||||||
|
.editor {
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.overlay-wrap {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.mode-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(
|
||||||
|
--spectrum-textfield-m-background-color,
|
||||||
|
var(--spectrum-global-color-gray-50)
|
||||||
|
);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
}
|
||||||
|
.prompt-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.prompt-body .switch-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding-drawer :global(.code-editor),
|
||||||
|
.binding-drawer :global(.code-editor > div) {
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,393 @@
|
||||||
|
<script>
|
||||||
|
import groupBy from "lodash/fp/groupBy"
|
||||||
|
import { convertToJS } from "@budibase/string-templates"
|
||||||
|
import { Input, Layout, ActionButton, Icon, Popover } from "@budibase/bbui"
|
||||||
|
import { handlebarsCompletions } from "constants/completions"
|
||||||
|
|
||||||
|
export let addHelper
|
||||||
|
export let addBinding
|
||||||
|
export let bindings
|
||||||
|
export let mode
|
||||||
|
export let allowHelpers
|
||||||
|
|
||||||
|
let search = ""
|
||||||
|
let popover
|
||||||
|
let popoverAnchor
|
||||||
|
let hoverTarget
|
||||||
|
let helpers = handlebarsCompletions()
|
||||||
|
|
||||||
|
let selectedCategory
|
||||||
|
|
||||||
|
$: searchRgx = new RegExp(search, "ig")
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
$: bindingIcons = bindings?.reduce((acc, ele) => {
|
||||||
|
if (ele.icon) {
|
||||||
|
acc[ele.category] = acc[ele.category] || ele.icon
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
$: categoryIcons = { ...bindingIcons, Helpers: "MagicWand" }
|
||||||
|
|
||||||
|
$: categories = Object.entries(groupBy("category", bindings))
|
||||||
|
$: categoryNames = getCategoryNames(categories)
|
||||||
|
|
||||||
|
$: filteredCategories = categories
|
||||||
|
.map(([name, categoryBindings]) => ({
|
||||||
|
name,
|
||||||
|
bindings: categoryBindings?.filter(binding => {
|
||||||
|
return binding.readableBinding.match(searchRgx)
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
.filter(category => {
|
||||||
|
return (
|
||||||
|
category.bindings?.length > 0 &&
|
||||||
|
(!selectedCategory ? true : selectedCategory === category.name)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
$: filteredHelpers = helpers?.filter(helper => {
|
||||||
|
return helper.label.match(searchRgx) || helper.description.match(searchRgx)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getHelperExample = (helper, js) => {
|
||||||
|
let example = helper.example || ""
|
||||||
|
if (js) {
|
||||||
|
example = convertToJS(example).split("\n")[0].split("= ")[1]
|
||||||
|
if (example === "null;") {
|
||||||
|
example = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return example || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoryNames = categories => {
|
||||||
|
let names = [...categories.map(cat => cat[0])]
|
||||||
|
if (allowHelpers) {
|
||||||
|
names.push("Helpers")
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="detailPopover">
|
||||||
|
<Popover
|
||||||
|
align="left-outside"
|
||||||
|
bind:this={popover}
|
||||||
|
anchor={popoverAnchor}
|
||||||
|
maxWidth={300}
|
||||||
|
dismissible={false}
|
||||||
|
>
|
||||||
|
<Layout gap="S">
|
||||||
|
<div class="helper">
|
||||||
|
{#if hoverTarget.title}
|
||||||
|
<div class="helper__name">{hoverTarget.title}</div>
|
||||||
|
{/if}
|
||||||
|
{#if hoverTarget.description}
|
||||||
|
<div class="helper__description">
|
||||||
|
{@html hoverTarget.description}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if hoverTarget.example}
|
||||||
|
<pre class="helper__example">{hoverTarget.example}</pre>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</Popover>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
{#if selectedCategory}
|
||||||
|
<div class="sub-section-back">
|
||||||
|
<ActionButton
|
||||||
|
secondary
|
||||||
|
icon={"ArrowLeft"}
|
||||||
|
on:click={() => {
|
||||||
|
selectedCategory = null
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !selectedCategory}
|
||||||
|
<div class="search">
|
||||||
|
<span class="search-input">
|
||||||
|
<Input
|
||||||
|
placeholder={"Search for bindings"}
|
||||||
|
autocomplete="off"
|
||||||
|
bind:value={search}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<span
|
||||||
|
class="search-input-icon"
|
||||||
|
on:click={() => {
|
||||||
|
if (!search) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
search = null
|
||||||
|
}}
|
||||||
|
class:searching={search}
|
||||||
|
>
|
||||||
|
<Icon name={search ? "Close" : "Search"} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !selectedCategory && !search}
|
||||||
|
<ul class="category-list">
|
||||||
|
{#each categoryNames as categoryName}
|
||||||
|
<li
|
||||||
|
on:click={() => {
|
||||||
|
selectedCategory = categoryName
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={categoryIcons[categoryName]} />
|
||||||
|
<span class="category-name">{categoryName} </span>
|
||||||
|
<span class="category-chevron"><Icon name="ChevronRight" /></span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedCategory || search}
|
||||||
|
{#each filteredCategories as category}
|
||||||
|
{#if category.bindings?.length}
|
||||||
|
<div class="sub-section">
|
||||||
|
<div class="cat-heading">
|
||||||
|
<Icon name={categoryIcons[category.name]} />{category.name}
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
{#each category.bindings as binding}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<li
|
||||||
|
class="binding"
|
||||||
|
on:mouseenter={e => {
|
||||||
|
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)}
|
||||||
|
>
|
||||||
|
<span class="binding__label">
|
||||||
|
{#if binding.display?.name}
|
||||||
|
{binding.display.name}
|
||||||
|
{:else if binding.fieldSchema?.name}
|
||||||
|
{binding.fieldSchema?.name}
|
||||||
|
{:else}
|
||||||
|
{binding.readableBinding}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{#if binding.display?.type || binding.fieldSchema?.type}
|
||||||
|
<span class="binding__typeWrap">
|
||||||
|
<span class="binding__type">
|
||||||
|
{binding.display?.type || binding.fieldSchema?.type}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if selectedCategory === "Helpers" || search}
|
||||||
|
{#if filteredHelpers?.length}
|
||||||
|
<div class="sub-section">
|
||||||
|
<div class="cat-heading">Helpers</div>
|
||||||
|
<ul class="helpers">
|
||||||
|
{#each filteredHelpers as helper}
|
||||||
|
<li
|
||||||
|
class="binding"
|
||||||
|
on:click={() => 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={() => {}}
|
||||||
|
>
|
||||||
|
<span class="binding__label">{helper.displayText}</span>
|
||||||
|
<span class="binding__typeWrap">
|
||||||
|
<span class="binding__type">function</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.search :global(input) {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0px;
|
||||||
|
background: none;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
padding: var(--spacing-m) var(--spacing-l);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-top: 0px;
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
border-right: 2px solid transparent;
|
||||||
|
margin-right: 1px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: var(--background);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-icon.searching {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.category-list {
|
||||||
|
padding: 0px var(--spacing-l);
|
||||||
|
padding-bottom: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.sub-section {
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
.sub-section-back {
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
padding-top: var(--spacing-xl);
|
||||||
|
padding-bottom: 0px;
|
||||||
|
}
|
||||||
|
.cat-heading {
|
||||||
|
margin-bottom: var(--spacing-l);
|
||||||
|
}
|
||||||
|
ul.helpers li * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
ul.category-list li {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
ul.category-list .category-name {
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
ul.category-list .category-chevron {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
ul.category-list .category-chevron :global(div.icon),
|
||||||
|
.cat-heading :global(div.icon) {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
li.binding {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
li.binding .binding__typeWrap {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.drawer-actions) {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-heading {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-heading {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
||||||
|
border-color 130ms ease-in-out;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
li:not(:last-of-type) {
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
li :global(*) {
|
||||||
|
transition: color 130ms ease-in-out;
|
||||||
|
}
|
||||||
|
li:hover {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding__label {
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding__type {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
padding: 2px 4px;
|
||||||
|
margin-left: 2px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -5,7 +5,7 @@
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher, setContext } from "svelte"
|
||||||
import { isJSBinding } from "@budibase/string-templates"
|
import { isJSBinding } from "@budibase/string-templates"
|
||||||
|
|
||||||
export let panel = ClientBindingPanel
|
export let panel = ClientBindingPanel
|
||||||
|
@ -34,6 +34,10 @@
|
||||||
bindingDrawer.hide()
|
bindingDrawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setContext("binding-drawer-actions", {
|
||||||
|
save: handleClose,
|
||||||
|
})
|
||||||
|
|
||||||
const onChange = (value, optionPicked) => {
|
const onChange = (value, optionPicked) => {
|
||||||
// Add HBS braces if picking binding
|
// Add HBS braces if picking binding
|
||||||
if (optionPicked && !options?.includes(value)) {
|
if (optionPicked && !options?.includes(value)) {
|
||||||
|
@ -63,7 +67,6 @@
|
||||||
on:pick={e => onChange(e.detail, true)}
|
on:pick={e => onChange(e.detail, true)}
|
||||||
on:blur={() => dispatch("blur")}
|
on:blur={() => dispatch("blur")}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
options={allOptions}
|
|
||||||
{error}
|
{error}
|
||||||
/>
|
/>
|
||||||
{#if !disabled}
|
{#if !disabled}
|
||||||
|
@ -77,6 +80,7 @@
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Add the objects on the left to enrich your text.
|
Add the objects on the left to enrich your text.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<Button cta slot="buttons" on:click={handleClose} disabled={!valid}>
|
<Button cta slot="buttons" on:click={handleClose} disabled={!valid}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -4,8 +4,11 @@
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
|
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher, setContext } from "svelte"
|
||||||
import { isJSBinding } from "@budibase/string-templates"
|
import { isJSBinding } from "@budibase/string-templates"
|
||||||
|
|
||||||
export let panel = ClientBindingPanel
|
export let panel = ClientBindingPanel
|
||||||
|
@ -20,6 +23,7 @@
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let drawerLeft
|
export let drawerLeft
|
||||||
|
export let key
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
|
@ -32,10 +36,15 @@
|
||||||
|
|
||||||
const saveBinding = () => {
|
const saveBinding = () => {
|
||||||
onChange(tempValue)
|
onChange(tempValue)
|
||||||
|
store.actions.settings.propertyFocus(null)
|
||||||
onBlur()
|
onBlur()
|
||||||
bindingDrawer.hide()
|
bindingDrawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setContext("binding-drawer-actions", {
|
||||||
|
save: saveBinding,
|
||||||
|
})
|
||||||
|
|
||||||
const onChange = value => {
|
const onChange = value => {
|
||||||
currentVal = readableToRuntimeBinding(bindings, value)
|
currentVal = readableToRuntimeBinding(bindings, value)
|
||||||
dispatch("change", currentVal)
|
dispatch("change", currentVal)
|
||||||
|
@ -58,12 +67,24 @@
|
||||||
{updateOnChange}
|
{updateOnChange}
|
||||||
/>
|
/>
|
||||||
{#if !disabled}
|
{#if !disabled}
|
||||||
<div class="icon" on:click={bindingDrawer.show}>
|
<div
|
||||||
|
class="icon"
|
||||||
|
on:click={() => {
|
||||||
|
store.actions.settings.propertyFocus(key)
|
||||||
|
bindingDrawer.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Icon size="S" name="FlashOn" />
|
<Icon size="S" name="FlashOn" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Drawer {fillWidth} bind:this={bindingDrawer} {title} left={drawerLeft}>
|
<Drawer
|
||||||
|
{fillWidth}
|
||||||
|
bind:this={bindingDrawer}
|
||||||
|
{title}
|
||||||
|
left={drawerLeft}
|
||||||
|
headless
|
||||||
|
>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Add the objects on the left to enrich your text.
|
Add the objects on the left to enrich your text.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
|
@ -17,14 +17,14 @@ import URLSelect from "./controls/URLSelect.svelte"
|
||||||
import OptionsEditor from "./controls/OptionsEditor/OptionsEditor.svelte"
|
import OptionsEditor from "./controls/OptionsEditor/OptionsEditor.svelte"
|
||||||
import FormFieldSelect from "./controls/FormFieldSelect.svelte"
|
import FormFieldSelect from "./controls/FormFieldSelect.svelte"
|
||||||
import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.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 ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
|
||||||
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
||||||
import BarButtonList from "./controls/BarButtonList.svelte"
|
import BarButtonList from "./controls/BarButtonList.svelte"
|
||||||
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
text: DrawerBindableCombobox,
|
text: DrawerBindableInput,
|
||||||
select: Select,
|
select: Select,
|
||||||
radio: RadioGroup,
|
radio: RadioGroup,
|
||||||
dataSource: DataSourceSelect,
|
dataSource: DataSourceSelect,
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
export let componentBindings = []
|
export let componentBindings = []
|
||||||
export let nested = false
|
export let nested = false
|
||||||
export let highlighted = false
|
export let highlighted = false
|
||||||
|
export let propertyFocus = false
|
||||||
export let info = null
|
export let info = null
|
||||||
|
|
||||||
$: nullishValue = value == null || value === ""
|
$: nullishValue = value == null || value === ""
|
||||||
|
@ -72,6 +73,10 @@
|
||||||
if (highlighted) {
|
if (highlighted) {
|
||||||
store.actions.settings.highlight(null)
|
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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -79,6 +84,7 @@
|
||||||
class="property-control"
|
class="property-control"
|
||||||
class:wide={!label || labelHidden}
|
class:wide={!label || labelHidden}
|
||||||
class:highlighted={highlighted && nullishValue}
|
class:highlighted={highlighted && nullishValue}
|
||||||
|
class:property-focus={propertyFocus}
|
||||||
>
|
>
|
||||||
{#if label && !labelHidden}
|
{#if label && !labelHidden}
|
||||||
<div class="label">
|
<div class="label">
|
||||||
|
@ -125,6 +131,14 @@
|
||||||
background: var(--spectrum-global-color-gray-300);
|
background: var(--spectrum-global-color-gray-300);
|
||||||
border-color: var(--spectrum-global-color-static-red-600);
|
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 {
|
.label {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
|
|
|
@ -101,7 +101,12 @@
|
||||||
}
|
}
|
||||||
// Ignore events when typing
|
// Ignore events when typing
|
||||||
const activeTag = document.activeElement?.tagName.toLowerCase()
|
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
|
return
|
||||||
}
|
}
|
||||||
// Key events are always for the selected component
|
// Key events are always for the selected component
|
||||||
|
|
|
@ -140,6 +140,7 @@
|
||||||
nested={setting.nested}
|
nested={setting.nested}
|
||||||
onChange={val => updateSetting(setting, val)}
|
onChange={val => updateSetting(setting, val)}
|
||||||
highlighted={$store.highlightedSettingKey === setting.key}
|
highlighted={$store.highlightedSettingKey === setting.key}
|
||||||
|
propertyFocus={$store.propertyFocus === setting.key}
|
||||||
info={setting.info}
|
info={setting.info}
|
||||||
props={{
|
props={{
|
||||||
// Generic settings
|
// Generic settings
|
||||||
|
|
120
yarn.lock
120
yarn.lock
|
@ -1889,6 +1889,84 @@
|
||||||
exec-sh "^0.3.2"
|
exec-sh "^0.3.2"
|
||||||
minimist "^1.2.0"
|
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":
|
"@colors/colors@1.5.0":
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||||
|
@ -3531,6 +3609,33 @@
|
||||||
validate-npm-package-name "^4.0.0"
|
validate-npm-package-name "^4.0.0"
|
||||||
yargs-parser "20.2.4"
|
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":
|
"@mapbox/node-pre-gyp@^1.0.0":
|
||||||
version "1.0.10"
|
version "1.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c"
|
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"
|
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
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:
|
cron-parser@^4.2.1:
|
||||||
version "4.7.1"
|
version "4.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.7.1.tgz#1e325a6a18e797a634ada1e2599ece0b6b5ed177"
|
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"
|
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575"
|
||||||
integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==
|
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:
|
stylehacks@^5.1.1:
|
||||||
version "5.1.1"
|
version "5.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9"
|
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9"
|
||||||
|
@ -25390,6 +25505,11 @@ w3c-hr-time@^1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
browser-process-hrtime "^1.0.0"
|
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:
|
w3c-xmlserializer@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a"
|
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a"
|
||||||
|
|
Loading…
Reference in New Issue