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
|
||||
} 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
|
||||
}
|
||||
|
|
|
@ -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,6 +57,7 @@
|
|||
transition:slide|local
|
||||
style={`width: ${width}; left: ${left};`}
|
||||
>
|
||||
{#if !headless}
|
||||
<header>
|
||||
<div class="text">
|
||||
<Heading size="XS">{title}</Heading>
|
||||
|
@ -62,6 +70,7 @@
|
|||
<slot name="buttons" />
|
||||
</div>
|
||||
</header>
|
||||
{/if}
|
||||
<slot name="body" />
|
||||
</section>
|
||||
</Portal>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
export let emphasized = false
|
||||
export let onTop = false
|
||||
export let size = "M"
|
||||
export let beforeSwitch = null
|
||||
|
||||
let thisSelected = undefined
|
||||
|
||||
|
@ -28,10 +29,15 @@
|
|||
thisSelected = selected
|
||||
dispatch("select", thisSelected)
|
||||
} else if ($tab.title !== 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 => {
|
||||
state.title = thisSelected
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
||||
return {
|
||||
label: runtime,
|
||||
type: value.type,
|
||||
description: value.description,
|
||||
category:
|
||||
const categoryName =
|
||||
idx === 0
|
||||
? "Trigger outputs"
|
||||
: isLoopBlock
|
||||
? "Loop Outputs"
|
||||
: `Step ${idx - loopBlockCount} outputs`,
|
||||
path: runtime,
|
||||
: `Step ${idx - loopBlockCount} outputs`
|
||||
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
|
||||
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,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
import groupBy from "lodash/fp/groupBy"
|
||||
import {
|
||||
Search,
|
||||
TextArea,
|
||||
DrawerContent,
|
||||
Tabs,
|
||||
Tab,
|
||||
Body,
|
||||
Layout,
|
||||
Button,
|
||||
ActionButton,
|
||||
Heading,
|
||||
Icon,
|
||||
Popover,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import {
|
||||
|
@ -23,11 +19,21 @@
|
|||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
import { handlebarsCompletions } from "constants/completions"
|
||||
import { addHBSBinding, addJSBinding } from "./utils"
|
||||
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
||||
import { store } from "builderStore"
|
||||
import { convertToJS } from "@budibase/string-templates"
|
||||
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()
|
||||
|
||||
|
@ -41,54 +47,21 @@
|
|||
export let allowJS = false
|
||||
export let allowHelpers = true
|
||||
|
||||
let helpers = handlebarsCompletions()
|
||||
const drawerActions = getContext("drawer-actions")
|
||||
const bindingDrawerActions = getContext("binding-drawer-actions")
|
||||
|
||||
let getCaretPosition
|
||||
let search = ""
|
||||
let insertAtPos
|
||||
let initialValueJS = typeof value === "string" && value?.startsWith("{{ js ")
|
||||
let mode = initialValueJS ? "JavaScript" : "Handlebars"
|
||||
let mode = initialValueJS ? "JavaScript" : "Text"
|
||||
let jsValue = initialValueJS ? value : null
|
||||
let hbsValue = initialValueJS ? null : value
|
||||
|
||||
let selectedCategory = null
|
||||
|
||||
let popover
|
||||
let popoverAnchor
|
||||
let hoverTarget
|
||||
let sidebar = true
|
||||
let targetMode = null
|
||||
|
||||
$: usingJS = mode === "JavaScript"
|
||||
$: searchRgx = new RegExp(search, "ig")
|
||||
$: categories = Object.entries(groupBy("category", bindings))
|
||||
|
||||
$: 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}")`)
|
||||
$: editorMode = mode == "JavaScript" ? EditorModes.JS : EditorModes.Handlebars
|
||||
$: bindingCompletions = bindingsToCompletions(bindings, editorMode)
|
||||
|
||||
const updateValue = 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
|
||||
const addHelper = (helper, js) => {
|
||||
let tempVal
|
||||
const onSelectHelper = (helper, js) => {
|
||||
const pos = getCaretPosition()
|
||||
const { start, end } = pos
|
||||
if (js) {
|
||||
const decoded = decodeJSBinding(jsValue)
|
||||
tempVal = jsValue = encodeJSBinding(
|
||||
addJSBinding(decoded, pos, helper.text, { helper: true })
|
||||
)
|
||||
let js = decodeJSBinding(jsValue)
|
||||
const insertVal = jsInsert(js, start, end, helper.text, { helper: true })
|
||||
insertAtPos({ start, end, value: insertVal })
|
||||
} 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
|
||||
const addBinding = (binding, { forceJS } = {}) => {
|
||||
const onSelectBinding = (binding, { forceJS } = {}) => {
|
||||
const { start, end } = getCaretPosition()
|
||||
if (usingJS || forceJS) {
|
||||
let js = decodeJSBinding(jsValue)
|
||||
js = addJSBinding(js, getCaretPosition(), binding.readableBinding)
|
||||
jsValue = encodeJSBinding(js)
|
||||
updateValue(jsValue)
|
||||
const insertVal = jsInsert(js, start, end, binding.readableBinding)
|
||||
insertAtPos({ start, end, value: insertVal })
|
||||
} else {
|
||||
hbsValue = addHBSBinding(
|
||||
hbsValue,
|
||||
getCaretPosition(),
|
||||
binding.readableBinding
|
||||
)
|
||||
updateValue(hbsValue)
|
||||
const insertVal = hbInsert(hbsValue, start, end, binding.readableBinding)
|
||||
insertAtPos({ start, end, value: insertVal })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,24 +112,25 @@
|
|||
updateValue(jsValue)
|
||||
}
|
||||
|
||||
const switchMode = () => {
|
||||
if (targetMode == "Text") {
|
||||
jsValue = null
|
||||
updateValue(jsValue)
|
||||
} else {
|
||||
hbsValue = null
|
||||
updateValue(hbsValue)
|
||||
}
|
||||
mode = targetMode + ""
|
||||
targetMode = null
|
||||
}
|
||||
|
||||
const convert = () => {
|
||||
const runtime = readableToRuntimeBinding(bindings, hbsValue)
|
||||
const runtimeJs = encodeJSBinding(convertToJS(runtime))
|
||||
jsValue = runtimeToReadableBinding(bindings, runtimeJs)
|
||||
hbsValue = null
|
||||
mode = "JavaScript"
|
||||
addBinding("", { 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 || ""
|
||||
onSelectBinding("", { forceJS: true })
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
@ -177,241 +138,250 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<span class="detailPopover">
|
||||
<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>
|
||||
|
||||
<span class="binding-drawer">
|
||||
<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">
|
||||
<Tabs selected={mode} on:select={onChangeMode}>
|
||||
<Tab title="Handlebars">
|
||||
<div class="main-content">
|
||||
<TextArea
|
||||
bind:getCaretPosition
|
||||
<Tabs
|
||||
selected={mode}
|
||||
on:select={onChangeMode}
|
||||
beforeSwitch={selectedMode => {
|
||||
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}
|
||||
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}
|
||||
<p class="syntax-error">
|
||||
Current Handlebars syntax is invalid, please check the guide
|
||||
<div class="syntax-error">
|
||||
Current Handlebars syntax is invalid, please check the
|
||||
guide
|
||||
<a href="https://handlebarsjs.com/guide/">here</a>
|
||||
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}
|
||||
</div>
|
||||
<div class="actions">
|
||||
{#if $admin.isDev && allowJS}
|
||||
<div class="convert">
|
||||
<Button secondary on:click={convert}>Convert to JS</Button>
|
||||
<ActionButton
|
||||
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>
|
||||
{/if}
|
||||
</div>
|
||||
</Tab>
|
||||
{#if allowJS}
|
||||
<Tab title="JavaScript">
|
||||
<div class="main-content">
|
||||
<Layout noPadding gap="XS">
|
||||
<CodeMirrorEditor
|
||||
bind:getCaretPosition
|
||||
height={200}
|
||||
<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 javascript
|
||||
</Button>
|
||||
<Button cta size="S" on:click={switchMode}>
|
||||
Yes - discard javascript
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<CodeEditor
|
||||
value={decodeJSBinding(jsValue)}
|
||||
on:change={onChangeJSValue}
|
||||
hints={codeMirrorHints}
|
||||
completions={[
|
||||
jsAutocomplete([
|
||||
...bindingCompletions,
|
||||
...getHelperCompletions(editorMode),
|
||||
]),
|
||||
]}
|
||||
mode={EditorModes.JS}
|
||||
bind:getCaretPosition
|
||||
bind:insertAtPos
|
||||
height="100%"
|
||||
/>
|
||||
<Body size="S">
|
||||
JavaScript expressions are executed as functions, so ensure that
|
||||
your expression returns a value.
|
||||
</Body>
|
||||
</Layout>
|
||||
</div>
|
||||
<div class="binding-footer">
|
||||
<div class="messaging">
|
||||
<Icon name="FlashOn" />
|
||||
<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>
|
||||
</Tab>
|
||||
{/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>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</span>
|
||||
|
||||
<style>
|
||||
ul.helpers li * {
|
||||
pointer-events: none;
|
||||
}
|
||||
ul.category-list li {
|
||||
.messaging {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
ul.category-list .category-name {
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
ul.category-list .category-chevron {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
ul.category-list .category-chevron :global(div.icon),
|
||||
.cat-heading :global(div.icon) {
|
||||
display: inline-block;
|
||||
.messaging-wrap {
|
||||
overflow: hidden;
|
||||
}
|
||||
li.binding {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.messaging-wrap > div {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
li.binding .binding__typeWrap {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
text-transform: capitalize;
|
||||
.binding-drawer :global(.drawer-contents) {
|
||||
height: unset;
|
||||
}
|
||||
.main :global(textarea) {
|
||||
min-height: 202px !important;
|
||||
|
@ -423,86 +393,23 @@
|
|||
padding: var(--spacing-s) var(--spacing-xl);
|
||||
}
|
||||
|
||||
.heading,
|
||||
.cat-heading {
|
||||
font-size: var(--font-size-s);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
.cat-heading {
|
||||
.main :global(.spectrum-Tabs div.drawer-actions) {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.main :global(.spectrum-Tabs-content),
|
||||
.main :global(.spectrum-Tabs-content .main-content) {
|
||||
margin-top: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.helper {
|
||||
.main :global(.spectrum-Tabs) {
|
||||
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 {
|
||||
padding-top: var(--spacing-m);
|
||||
color: var(--red);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
@ -511,7 +418,65 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.convert {
|
||||
padding-top: var(--spacing-m);
|
||||
.binding-footer {
|
||||
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>
|
||||
|
|
|
@ -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,
|
||||
} 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 @@
|
|||
<svelte:fragment slot="description">
|
||||
Add the objects on the left to enrich your text.
|
||||
</svelte:fragment>
|
||||
|
||||
<Button cta slot="buttons" on:click={handleClose} disabled={!valid}>
|
||||
Save
|
||||
</Button>
|
||||
|
|
|
@ -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}
|
||||
<div class="icon" on:click={bindingDrawer.show}>
|
||||
<div
|
||||
class="icon"
|
||||
on:click={() => {
|
||||
store.actions.settings.propertyFocus(key)
|
||||
bindingDrawer.show()
|
||||
}}
|
||||
>
|
||||
<Icon size="S" name="FlashOn" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Drawer {fillWidth} bind:this={bindingDrawer} {title} left={drawerLeft}>
|
||||
<Drawer
|
||||
{fillWidth}
|
||||
bind:this={bindingDrawer}
|
||||
{title}
|
||||
left={drawerLeft}
|
||||
headless
|
||||
>
|
||||
<svelte:fragment slot="description">
|
||||
Add the objects on the left to enrich your text.
|
||||
</svelte:fragment>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -79,6 +84,7 @@
|
|||
class="property-control"
|
||||
class:wide={!label || labelHidden}
|
||||
class:highlighted={highlighted && nullishValue}
|
||||
class:property-focus={propertyFocus}
|
||||
>
|
||||
{#if label && !labelHidden}
|
||||
<div class="label">
|
||||
|
@ -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%);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
120
yarn.lock
120
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"
|
||||
|
|
Loading…
Reference in New Issue