Merge branch 'master' into rewrite-relationship-field
This commit is contained in:
commit
6008c4e41e
|
@ -64,7 +64,7 @@
|
||||||
import { setContext, createEventDispatcher, onDestroy } from "svelte"
|
import { setContext, createEventDispatcher, onDestroy } from "svelte"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
|
|
||||||
export let title
|
export let title = ""
|
||||||
export let forceModal = false
|
export let forceModal = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
import type { KeyboardEventHandler } from "svelte/elements"
|
import type { KeyboardEventHandler } from "svelte/elements"
|
||||||
import { PopoverAlignment } from "../constants"
|
import { PopoverAlignment } from "../constants"
|
||||||
|
|
||||||
export let anchor: HTMLElement
|
export let anchor: HTMLElement | undefined
|
||||||
export let align: PopoverAlignment | `${PopoverAlignment}` =
|
export let align: PopoverAlignment | `${PopoverAlignment}` =
|
||||||
PopoverAlignment.Right
|
PopoverAlignment.Right
|
||||||
export let portalTarget: string | undefined = undefined
|
export let portalTarget: string | undefined = undefined
|
||||||
|
|
|
@ -27,12 +27,11 @@
|
||||||
} from "../CodeEditor"
|
} from "../CodeEditor"
|
||||||
import BindingSidePanel from "./BindingSidePanel.svelte"
|
import BindingSidePanel from "./BindingSidePanel.svelte"
|
||||||
import EvaluationSidePanel from "./EvaluationSidePanel.svelte"
|
import EvaluationSidePanel from "./EvaluationSidePanel.svelte"
|
||||||
import SnippetSidePanel from "./SnippetSidePanel.svelte"
|
|
||||||
import { BindingHelpers } from "./utils"
|
import { BindingHelpers } from "./utils"
|
||||||
import { capitalise } from "@/helpers"
|
import { capitalise } from "@/helpers"
|
||||||
import { Utils, JsonFormatter } from "@budibase/frontend-core"
|
import { Utils, JsonFormatter } from "@budibase/frontend-core"
|
||||||
import { licensing } from "@/stores/portal"
|
import { licensing } from "@/stores/portal"
|
||||||
import { BindingMode, SidePanel } from "@budibase/types"
|
import { BindingMode } from "@budibase/types"
|
||||||
import type {
|
import type {
|
||||||
EnrichedBinding,
|
EnrichedBinding,
|
||||||
Snippet,
|
Snippet,
|
||||||
|
@ -44,6 +43,8 @@
|
||||||
import type { Log } from "@budibase/string-templates"
|
import type { Log } from "@budibase/string-templates"
|
||||||
import type { CodeValidator } from "@/types"
|
import type { CodeValidator } from "@/types"
|
||||||
|
|
||||||
|
type SidePanel = "Bindings" | "Evaluation"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let bindings: EnrichedBinding[] = []
|
export let bindings: EnrichedBinding[] = []
|
||||||
|
@ -55,7 +56,7 @@
|
||||||
export let context = null
|
export let context = null
|
||||||
export let snippets: Snippet[] | null = null
|
export let snippets: Snippet[] | null = null
|
||||||
export let autofocusEditor = false
|
export let autofocusEditor = false
|
||||||
export let placeholder = null
|
export let placeholder: string | null = null
|
||||||
export let showTabBar = true
|
export let showTabBar = true
|
||||||
|
|
||||||
let mode: BindingMode
|
let mode: BindingMode
|
||||||
|
@ -71,14 +72,13 @@
|
||||||
let expressionError: string | undefined
|
let expressionError: string | undefined
|
||||||
let evaluating = false
|
let evaluating = false
|
||||||
|
|
||||||
$: useSnippets = allowSnippets && !$licensing.isFreePlan
|
const SidePanelIcons: Record<SidePanel, string> = {
|
||||||
|
Bindings: "FlashOn",
|
||||||
|
Evaluation: "Play",
|
||||||
|
}
|
||||||
|
|
||||||
$: editorModeOptions = getModeOptions(allowHBS, allowJS)
|
$: editorModeOptions = getModeOptions(allowHBS, allowJS)
|
||||||
$: sidePanelOptions = getSidePanelOptions(
|
$: sidePanelOptions = getSidePanelOptions(bindings, context)
|
||||||
bindings,
|
|
||||||
context,
|
|
||||||
allowSnippets,
|
|
||||||
mode
|
|
||||||
)
|
|
||||||
$: enrichedBindings = enrichBindings(bindings, context, snippets)
|
$: enrichedBindings = enrichBindings(bindings, context, snippets)
|
||||||
$: usingJS = mode === BindingMode.JavaScript
|
$: usingJS = mode === BindingMode.JavaScript
|
||||||
$: editorMode =
|
$: editorMode =
|
||||||
|
@ -93,7 +93,9 @@
|
||||||
$: bindingOptions = bindingsToCompletions(enrichedBindings, editorMode)
|
$: bindingOptions = bindingsToCompletions(enrichedBindings, editorMode)
|
||||||
$: helperOptions = allowHelpers ? getHelperCompletions(editorMode) : []
|
$: helperOptions = allowHelpers ? getHelperCompletions(editorMode) : []
|
||||||
$: snippetsOptions =
|
$: snippetsOptions =
|
||||||
usingJS && useSnippets && snippets?.length ? snippets : []
|
usingJS && allowSnippets && !$licensing.isFreePlan && snippets?.length
|
||||||
|
? snippets
|
||||||
|
: []
|
||||||
|
|
||||||
$: completions = !usingJS
|
$: completions = !usingJS
|
||||||
? [hbAutocomplete([...bindingOptions, ...helperOptions])]
|
? [hbAutocomplete([...bindingOptions, ...helperOptions])]
|
||||||
|
@ -137,21 +139,13 @@
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSidePanelOptions = (
|
const getSidePanelOptions = (bindings: EnrichedBinding[], context: any) => {
|
||||||
bindings: EnrichedBinding[],
|
let options: SidePanel[] = []
|
||||||
context: any,
|
|
||||||
useSnippets: boolean,
|
|
||||||
mode: BindingMode | null
|
|
||||||
) => {
|
|
||||||
let options = []
|
|
||||||
if (bindings?.length) {
|
if (bindings?.length) {
|
||||||
options.push(SidePanel.Bindings)
|
options.push("Bindings")
|
||||||
}
|
}
|
||||||
if (context && Object.keys(context).length > 0) {
|
if (context && Object.keys(context).length > 0) {
|
||||||
options.push(SidePanel.Evaluation)
|
options.push("Evaluation")
|
||||||
}
|
|
||||||
if (useSnippets && mode === BindingMode.JavaScript) {
|
|
||||||
options.push(SidePanel.Snippets)
|
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
@ -342,14 +336,15 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="side-tabs">
|
<div class="side-tabs">
|
||||||
{#each sidePanelOptions as panel}
|
{#each sidePanelOptions as panelOption}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
size="M"
|
size="M"
|
||||||
quiet
|
quiet
|
||||||
selected={sidePanel === panel}
|
selected={sidePanel === panelOption}
|
||||||
on:click={() => changeSidePanel(panel)}
|
on:click={() => changeSidePanel(panelOption)}
|
||||||
|
tooltip={panelOption}
|
||||||
>
|
>
|
||||||
<Icon name={panel} size="S" />
|
<Icon name={SidePanelIcons[panelOption]} size="S" />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -415,16 +410,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="side" class:visible={!!sidePanel}>
|
<div class="side" class:visible={!!sidePanel}>
|
||||||
{#if sidePanel === SidePanel.Bindings}
|
{#if sidePanel === "Bindings"}
|
||||||
<BindingSidePanel
|
<BindingSidePanel
|
||||||
bindings={enrichedBindings}
|
bindings={enrichedBindings}
|
||||||
{allowHelpers}
|
{allowHelpers}
|
||||||
|
{allowSnippets}
|
||||||
{context}
|
{context}
|
||||||
addHelper={onSelectHelper}
|
addHelper={onSelectHelper}
|
||||||
addBinding={onSelectBinding}
|
addBinding={onSelectBinding}
|
||||||
|
{addSnippet}
|
||||||
{mode}
|
{mode}
|
||||||
|
{snippets}
|
||||||
/>
|
/>
|
||||||
{:else if sidePanel === SidePanel.Evaluation}
|
{:else if sidePanel === "Evaluation"}
|
||||||
<EvaluationSidePanel
|
<EvaluationSidePanel
|
||||||
{expressionResult}
|
{expressionResult}
|
||||||
{expressionError}
|
{expressionError}
|
||||||
|
@ -432,8 +430,6 @@
|
||||||
{evaluating}
|
{evaluating}
|
||||||
expression={editorValue ? editorValue : ""}
|
expression={editorValue ? editorValue : ""}
|
||||||
/>
|
/>
|
||||||
{:else if sidePanel === SidePanel.Snippets}
|
|
||||||
<SnippetSidePanel {addSnippet} {snippets} />
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,31 +1,52 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import groupBy from "lodash/fp/groupBy"
|
import groupBy from "lodash/fp/groupBy"
|
||||||
import { convertToJS } from "@budibase/string-templates"
|
import { convertToJS } from "@budibase/string-templates"
|
||||||
import { Input, Layout, Icon, Popover } from "@budibase/bbui"
|
import { licensing } from "@/stores/portal"
|
||||||
|
import {
|
||||||
|
Input,
|
||||||
|
Layout,
|
||||||
|
Icon,
|
||||||
|
Popover,
|
||||||
|
Tags,
|
||||||
|
Tag,
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { handlebarsCompletions } from "@/constants/completions"
|
import { handlebarsCompletions } from "@/constants/completions"
|
||||||
import type { EnrichedBinding, Helper } from "@budibase/types"
|
import type { EnrichedBinding, Helper, Snippet } from "@budibase/types"
|
||||||
import { BindingMode } from "@budibase/types"
|
import { BindingMode } from "@budibase/types"
|
||||||
|
import { EditorModes } from "../CodeEditor"
|
||||||
|
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
||||||
|
|
||||||
|
import SnippetDrawer from "./SnippetDrawer.svelte"
|
||||||
|
import UpgradeButton from "@/pages/builder/portal/_components/UpgradeButton.svelte"
|
||||||
|
|
||||||
export let addHelper: (_helper: Helper, _js?: boolean) => void
|
export let addHelper: (_helper: Helper, _js?: boolean) => void
|
||||||
export let addBinding: (_binding: EnrichedBinding) => void
|
export let addBinding: (_binding: EnrichedBinding) => void
|
||||||
|
export let addSnippet: (_snippet: Snippet) => void
|
||||||
export let bindings: EnrichedBinding[]
|
export let bindings: EnrichedBinding[]
|
||||||
|
export let snippets: Snippet[] | null
|
||||||
export let mode: BindingMode
|
export let mode: BindingMode
|
||||||
export let allowHelpers: boolean
|
export let allowHelpers: boolean
|
||||||
|
export let allowSnippets: boolean
|
||||||
export let context = null
|
export let context = null
|
||||||
|
|
||||||
let search = ""
|
let search = ""
|
||||||
let searching = false
|
let searching = false
|
||||||
let popover: Popover
|
let popover: Popover
|
||||||
let popoverAnchor: HTMLElement | null
|
let popoverAnchor: HTMLElement | undefined
|
||||||
let hoverTarget: {
|
let hoverTarget: {
|
||||||
helper: boolean
|
type: "binding" | "helper" | "snippet"
|
||||||
code: string
|
code: string
|
||||||
description?: string
|
description?: string
|
||||||
} | null
|
} | null
|
||||||
let helpers = handlebarsCompletions()
|
let helpers = handlebarsCompletions()
|
||||||
let selectedCategory: string | null
|
let selectedCategory: string | null
|
||||||
let hideTimeout: ReturnType<typeof setTimeout> | null
|
let hideTimeout: ReturnType<typeof setTimeout> | null
|
||||||
|
let snippetDrawer: SnippetDrawer
|
||||||
|
let editableSnippet: Snippet | null
|
||||||
|
|
||||||
|
$: enableSnippets = !$licensing.isFreePlan
|
||||||
$: bindingIcons = bindings?.reduce<Record<string, string>>((acc, ele) => {
|
$: bindingIcons = bindings?.reduce<Record<string, string>>((acc, ele) => {
|
||||||
if (ele.icon) {
|
if (ele.icon) {
|
||||||
acc[ele.category] = acc[ele.category] || ele.icon
|
acc[ele.category] = acc[ele.category] || ele.icon
|
||||||
|
@ -35,9 +56,14 @@
|
||||||
$: categoryIcons = {
|
$: categoryIcons = {
|
||||||
...bindingIcons,
|
...bindingIcons,
|
||||||
Helpers: "MagicWand",
|
Helpers: "MagicWand",
|
||||||
|
Snippets: "Code",
|
||||||
} as Record<string, string>
|
} as Record<string, string>
|
||||||
$: categories = Object.entries(groupBy("category", bindings))
|
$: categories = Object.entries(groupBy("category", bindings))
|
||||||
$: categoryNames = getCategoryNames(categories)
|
|
||||||
|
$: categoryNames = getCategoryNames(
|
||||||
|
categories,
|
||||||
|
allowSnippets && mode === BindingMode.JavaScript
|
||||||
|
)
|
||||||
$: searchRgx = new RegExp(search, "ig")
|
$: searchRgx = new RegExp(search, "ig")
|
||||||
$: filteredCategories = categories
|
$: filteredCategories = categories
|
||||||
.map(([name, categoryBindings]) => ({
|
.map(([name, categoryBindings]) => ({
|
||||||
|
@ -61,6 +87,17 @@
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$: filteredSnippets = getFilteredSnippets(
|
||||||
|
enableSnippets,
|
||||||
|
snippets || [],
|
||||||
|
search
|
||||||
|
)
|
||||||
|
|
||||||
|
function onModeChange(_mode: BindingMode) {
|
||||||
|
selectedCategory = null
|
||||||
|
}
|
||||||
|
$: onModeChange(mode)
|
||||||
|
|
||||||
const getHelperExample = (helper: Helper, js: boolean) => {
|
const getHelperExample = (helper: Helper, js: boolean) => {
|
||||||
let example = helper.example || ""
|
let example = helper.example || ""
|
||||||
if (js) {
|
if (js) {
|
||||||
|
@ -72,11 +109,17 @@
|
||||||
return example || ""
|
return example || ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCategoryNames = (categories: [string, EnrichedBinding[]][]) => {
|
const getCategoryNames = (
|
||||||
|
categories: [string, EnrichedBinding[]][],
|
||||||
|
showSnippets: boolean
|
||||||
|
) => {
|
||||||
const names = [...categories.map(cat => cat[0])]
|
const names = [...categories.map(cat => cat[0])]
|
||||||
if (allowHelpers) {
|
if (allowHelpers) {
|
||||||
names.push("Helpers")
|
names.push("Helpers")
|
||||||
}
|
}
|
||||||
|
if (showSnippets) {
|
||||||
|
names.push("Snippets")
|
||||||
|
}
|
||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,20 +133,20 @@
|
||||||
stopHidingPopover()
|
stopHidingPopover()
|
||||||
popoverAnchor = target
|
popoverAnchor = target
|
||||||
hoverTarget = {
|
hoverTarget = {
|
||||||
helper: false,
|
type: "binding",
|
||||||
code: binding.valueHTML,
|
code: binding.valueHTML,
|
||||||
}
|
}
|
||||||
popover.show()
|
popover.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
const showHelperPopover = (helper: any, target: HTMLElement) => {
|
const showHelperPopover = (helper: Helper, target: HTMLElement) => {
|
||||||
stopHidingPopover()
|
stopHidingPopover()
|
||||||
if (!helper.displayText && helper.description) {
|
if (!helper.displayText && helper.description) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
popoverAnchor = target
|
popoverAnchor = target
|
||||||
hoverTarget = {
|
hoverTarget = {
|
||||||
helper: true,
|
type: "helper",
|
||||||
description: helper.description,
|
description: helper.description,
|
||||||
code: getHelperExample(helper, mode === BindingMode.JavaScript),
|
code: getHelperExample(helper, mode === BindingMode.JavaScript),
|
||||||
}
|
}
|
||||||
|
@ -113,7 +156,7 @@
|
||||||
const hidePopover = () => {
|
const hidePopover = () => {
|
||||||
hideTimeout = setTimeout(() => {
|
hideTimeout = setTimeout(() => {
|
||||||
popover.hide()
|
popover.hide()
|
||||||
popoverAnchor = null
|
popoverAnchor = undefined
|
||||||
hoverTarget = null
|
hoverTarget = null
|
||||||
hideTimeout = null
|
hideTimeout = null
|
||||||
}, 100)
|
}, 100)
|
||||||
|
@ -136,10 +179,51 @@
|
||||||
searching = false
|
searching = false
|
||||||
search = ""
|
search = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFilteredSnippets = (
|
||||||
|
enableSnippets: boolean,
|
||||||
|
snippets: Snippet[],
|
||||||
|
search: string
|
||||||
|
) => {
|
||||||
|
if (!enableSnippets || !snippets.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (!search?.length) {
|
||||||
|
return snippets
|
||||||
|
}
|
||||||
|
return snippets.filter(snippet =>
|
||||||
|
snippet.name.toLowerCase().includes(search.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const showSnippet = (snippet: Snippet, target: HTMLElement) => {
|
||||||
|
stopHidingPopover()
|
||||||
|
if (!snippet.code) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
popoverAnchor = target
|
||||||
|
hoverTarget = {
|
||||||
|
type: "snippet",
|
||||||
|
code: snippet.code,
|
||||||
|
}
|
||||||
|
|
||||||
|
popover.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSnippet = () => {
|
||||||
|
editableSnippet = null
|
||||||
|
snippetDrawer.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const editSnippet = (e: Event, snippet: Snippet) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
editableSnippet = snippet
|
||||||
|
snippetDrawer.show()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if popoverAnchor && hoverTarget}
|
<Popover
|
||||||
<Popover
|
|
||||||
align="left-outside"
|
align="left-outside"
|
||||||
bind:this={popover}
|
bind:this={popover}
|
||||||
anchor={popoverAnchor}
|
anchor={popoverAnchor}
|
||||||
|
@ -149,8 +233,12 @@
|
||||||
dismissible={false}
|
dismissible={false}
|
||||||
on:mouseenter={stopHidingPopover}
|
on:mouseenter={stopHidingPopover}
|
||||||
on:mouseleave={hidePopover}
|
on:mouseleave={hidePopover}
|
||||||
|
>
|
||||||
|
{#if hoverTarget}
|
||||||
|
<div
|
||||||
|
class="binding-popover"
|
||||||
|
class:has-code={hoverTarget.type !== "binding"}
|
||||||
>
|
>
|
||||||
<div class="binding-popover" class:helper={hoverTarget.helper}>
|
|
||||||
{#if hoverTarget.description}
|
{#if hoverTarget.description}
|
||||||
<div>
|
<div>
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||||
|
@ -158,12 +246,20 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if hoverTarget.code}
|
{#if hoverTarget.code}
|
||||||
|
{#if mode === BindingMode.JavaScript}
|
||||||
|
<CodeEditor
|
||||||
|
value={hoverTarget.code?.trim()}
|
||||||
|
mode={EditorModes.JS}
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
{:else if mode === BindingMode.Text}
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||||
<pre>{@html hoverTarget.code}</pre>
|
<pre>{@html hoverTarget.code}</pre>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
{/if}
|
||||||
{/if}
|
</Popover>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
|
@ -178,6 +274,25 @@
|
||||||
on:click={() => (selectedCategory = null)}
|
on:click={() => (selectedCategory = null)}
|
||||||
/>
|
/>
|
||||||
{selectedCategory}
|
{selectedCategory}
|
||||||
|
{#if selectedCategory === "Snippets"}
|
||||||
|
{#if enableSnippets}
|
||||||
|
<div class="add-snippet-button">
|
||||||
|
<Icon
|
||||||
|
size="S"
|
||||||
|
name="Add"
|
||||||
|
hoverable
|
||||||
|
newStyles
|
||||||
|
on:click={createSnippet}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="title">
|
||||||
|
<Tags>
|
||||||
|
<Tag icon="LockClosed">Premium</Tag>
|
||||||
|
</Tags>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -281,7 +396,6 @@
|
||||||
class="binding"
|
class="binding"
|
||||||
on:mouseenter={e =>
|
on:mouseenter={e =>
|
||||||
showHelperPopover(helper, e.currentTarget)}
|
showHelperPopover(helper, e.currentTarget)}
|
||||||
on:mouseleave={hidePopover}
|
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
addHelper(helper, mode === BindingMode.JavaScript)}
|
addHelper(helper, mode === BindingMode.JavaScript)}
|
||||||
>
|
>
|
||||||
|
@ -295,10 +409,48 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedCategory === "Snippets" || search}
|
||||||
|
<div class="snippet-list">
|
||||||
|
{#if enableSnippets && filteredSnippets.length}
|
||||||
|
{#each filteredSnippets as snippet}
|
||||||
|
<li
|
||||||
|
class="snippet"
|
||||||
|
on:mouseenter={e => showSnippet(snippet, e.currentTarget)}
|
||||||
|
on:mouseleave={hidePopover}
|
||||||
|
on:click={() => addSnippet(snippet)}
|
||||||
|
>
|
||||||
|
{snippet.name}
|
||||||
|
<Icon
|
||||||
|
name="Edit"
|
||||||
|
hoverable
|
||||||
|
newStyles
|
||||||
|
size="S"
|
||||||
|
on:click={e => editSnippet(e, snippet)}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
{:else if !search}
|
||||||
|
<div class="upgrade">
|
||||||
|
<Body size="S">
|
||||||
|
Snippets let you create reusable JS functions and values that
|
||||||
|
can all be managed in one place
|
||||||
|
</Body>
|
||||||
|
{#if enableSnippets}
|
||||||
|
<Button cta on:click={createSnippet}>Create snippet</Button>
|
||||||
|
{:else}
|
||||||
|
<UpgradeButton />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SnippetDrawer bind:this={snippetDrawer} snippet={editableSnippet} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.binding-side-panel {
|
.binding-side-panel {
|
||||||
border-left: var(--border-light);
|
border-left: var(--border-light);
|
||||||
|
@ -363,6 +515,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
li.binding .binding__typeWrap {
|
li.binding .binding__typeWrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -438,7 +591,7 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.binding-popover.helper pre {
|
.binding-popover.has-code pre {
|
||||||
color: var(--spectrum-global-color-blue-700);
|
color: var(--spectrum-global-color-blue-700);
|
||||||
}
|
}
|
||||||
.binding-popover pre :global(span) {
|
.binding-popover pre :global(span) {
|
||||||
|
@ -450,7 +603,50 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.binding-popover.helper :global(code) {
|
.binding-popover.has-code :global(code) {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.binding-popover.has-code :global(.cm-line),
|
||||||
|
.binding-popover.has-code :global(.cm-content) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Snippets */
|
||||||
|
.add-snippet-button {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.snippet-list {
|
||||||
|
padding: 0 var(--spacing-l);
|
||||||
|
padding-bottom: var(--spacing-l);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.snippet {
|
||||||
|
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-out, color 130ms ease-out,
|
||||||
|
border-color 130ms ease-out;
|
||||||
|
word-wrap: break-word;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.snippet:hover {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Upgrade */
|
||||||
|
.upgrade {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.upgrade :global(p) {
|
||||||
|
text-align: center;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Drawer,
|
Drawer,
|
||||||
|
@ -14,19 +14,27 @@
|
||||||
import { getSequentialName } from "@/helpers/duplicate"
|
import { getSequentialName } from "@/helpers/duplicate"
|
||||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||||
import { ValidSnippetNameRegex } from "@budibase/shared-core"
|
import { ValidSnippetNameRegex } from "@budibase/shared-core"
|
||||||
|
import type { Snippet } from "@budibase/types"
|
||||||
|
|
||||||
export let snippet
|
export const show = () => {
|
||||||
|
if (!snippet) {
|
||||||
export const show = () => drawer.show()
|
key = Math.random().toString()
|
||||||
|
// Reset state when creating multiple snippets
|
||||||
|
code = ""
|
||||||
|
name = defaultName
|
||||||
|
}
|
||||||
|
drawer.show()
|
||||||
|
}
|
||||||
export const hide = () => drawer.hide()
|
export const hide = () => drawer.hide()
|
||||||
|
export let snippet: Snippet | null
|
||||||
|
|
||||||
const firstCharNumberRegex = /^[0-9].*$/
|
const firstCharNumberRegex = /^[0-9].*$/
|
||||||
|
|
||||||
let drawer
|
let drawer: Drawer
|
||||||
let name = ""
|
let name = ""
|
||||||
let code = ""
|
let code = ""
|
||||||
let loading = false
|
let loading = false
|
||||||
let deleteConfirmationDialog
|
let deleteConfirmationDialog: ConfirmDialog
|
||||||
|
|
||||||
$: defaultName = getSequentialName($snippets, "MySnippet", {
|
$: defaultName = getSequentialName($snippets, "MySnippet", {
|
||||||
getName: x => x.name,
|
getName: x => x.name,
|
||||||
|
@ -40,11 +48,11 @@
|
||||||
const saveSnippet = async () => {
|
const saveSnippet = async () => {
|
||||||
loading = true
|
loading = true
|
||||||
try {
|
try {
|
||||||
const newSnippet = { name, code: rawJS }
|
const newSnippet: Snippet = { name, code: rawJS || "" }
|
||||||
await snippets.saveSnippet(newSnippet)
|
await snippets.saveSnippet(newSnippet)
|
||||||
drawer.hide()
|
drawer.hide()
|
||||||
notifications.success(`Snippet ${newSnippet.name} saved`)
|
notifications.success(`Snippet ${newSnippet.name} saved`)
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
notifications.error(error.message || "Error saving snippet")
|
notifications.error(error.message || "Error saving snippet")
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
|
@ -53,7 +61,9 @@
|
||||||
const deleteSnippet = async () => {
|
const deleteSnippet = async () => {
|
||||||
loading = true
|
loading = true
|
||||||
try {
|
try {
|
||||||
|
if (snippet) {
|
||||||
await snippets.deleteSnippet(snippet.name)
|
await snippets.deleteSnippet(snippet.name)
|
||||||
|
}
|
||||||
drawer.hide()
|
drawer.hide()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error deleting snippet")
|
notifications.error("Error deleting snippet")
|
||||||
|
@ -61,7 +71,7 @@
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateName = (name, snippets) => {
|
const validateName = (name: string, snippets: Snippet[]) => {
|
||||||
if (!name?.length) {
|
if (!name?.length) {
|
||||||
return "Name is required"
|
return "Name is required"
|
||||||
}
|
}
|
||||||
|
@ -108,7 +118,11 @@
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<Button cta on:click={saveSnippet} disabled={!code || loading || nameError}>
|
<Button
|
||||||
|
cta
|
||||||
|
on:click={saveSnippet}
|
||||||
|
disabled={!code || loading || !!nameError}
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
@ -124,9 +138,7 @@
|
||||||
value={code}
|
value={code}
|
||||||
on:change={e => (code = e.detail)}
|
on:change={e => (code = e.detail)}
|
||||||
>
|
>
|
||||||
<div slot="tabs">
|
|
||||||
<Input placeholder="Name" />
|
<Input placeholder="Name" />
|
||||||
</div>
|
|
||||||
</BindingPanel>
|
</BindingPanel>
|
||||||
{/key}
|
{/key}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
|
@ -1,278 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
Input,
|
|
||||||
Layout,
|
|
||||||
Icon,
|
|
||||||
Popover,
|
|
||||||
Tags,
|
|
||||||
Tag,
|
|
||||||
Body,
|
|
||||||
Button,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import CodeEditor from "@/components/common/CodeEditor/CodeEditor.svelte"
|
|
||||||
import { EditorModes } from "@/components/common/CodeEditor"
|
|
||||||
import SnippetDrawer from "./SnippetDrawer.svelte"
|
|
||||||
import { licensing } from "@/stores/portal"
|
|
||||||
import UpgradeButton from "@/pages/builder/portal/_components/UpgradeButton.svelte"
|
|
||||||
|
|
||||||
export let addSnippet
|
|
||||||
export let snippets
|
|
||||||
|
|
||||||
let search = ""
|
|
||||||
let searching = false
|
|
||||||
let popover
|
|
||||||
let popoverAnchor
|
|
||||||
let hoveredSnippet
|
|
||||||
let hideTimeout
|
|
||||||
let snippetDrawer
|
|
||||||
let editableSnippet
|
|
||||||
|
|
||||||
$: enableSnippets = !$licensing.isFreePlan
|
|
||||||
$: filteredSnippets = getFilteredSnippets(enableSnippets, snippets, search)
|
|
||||||
|
|
||||||
const getFilteredSnippets = (enableSnippets, snippets, search) => {
|
|
||||||
if (!enableSnippets || !snippets?.length) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
if (!search?.length) {
|
|
||||||
return snippets
|
|
||||||
}
|
|
||||||
return snippets.filter(snippet =>
|
|
||||||
snippet.name.toLowerCase().includes(search.toLowerCase())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const showSnippet = (snippet, target) => {
|
|
||||||
stopHidingPopover()
|
|
||||||
popoverAnchor = target
|
|
||||||
hoveredSnippet = snippet
|
|
||||||
popover.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const hidePopover = () => {
|
|
||||||
hideTimeout = setTimeout(() => {
|
|
||||||
popover.hide()
|
|
||||||
popoverAnchor = null
|
|
||||||
hoveredSnippet = null
|
|
||||||
hideTimeout = null
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopHidingPopover = () => {
|
|
||||||
if (hideTimeout) {
|
|
||||||
clearTimeout(hideTimeout)
|
|
||||||
hideTimeout = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const startSearching = () => {
|
|
||||||
searching = true
|
|
||||||
search = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopSearching = () => {
|
|
||||||
searching = false
|
|
||||||
search = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const createSnippet = () => {
|
|
||||||
editableSnippet = null
|
|
||||||
snippetDrawer.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const editSnippet = (e, snippet) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
editableSnippet = snippet
|
|
||||||
snippetDrawer.show()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<div class="snippet-side-panel">
|
|
||||||
<Layout noPadding gap="S">
|
|
||||||
<div class="header">
|
|
||||||
{#if enableSnippets}
|
|
||||||
{#if searching}
|
|
||||||
<div class="search-input">
|
|
||||||
<Input
|
|
||||||
placeholder="Search for snippets"
|
|
||||||
autocomplete="off"
|
|
||||||
bind:value={search}
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Icon
|
|
||||||
size="S"
|
|
||||||
name="Close"
|
|
||||||
hoverable
|
|
||||||
newStyles
|
|
||||||
on:click={stopSearching}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div class="title">Snippets</div>
|
|
||||||
<Icon
|
|
||||||
size="S"
|
|
||||||
name="Search"
|
|
||||||
hoverable
|
|
||||||
newStyles
|
|
||||||
on:click={startSearching}
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
size="S"
|
|
||||||
name="Add"
|
|
||||||
hoverable
|
|
||||||
newStyles
|
|
||||||
on:click={createSnippet}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<div class="title">
|
|
||||||
Snippets
|
|
||||||
<Tags>
|
|
||||||
<Tag icon="LockClosed">Premium</Tag>
|
|
||||||
</Tags>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="snippet-list">
|
|
||||||
{#if enableSnippets && filteredSnippets?.length}
|
|
||||||
{#each filteredSnippets as snippet}
|
|
||||||
<div
|
|
||||||
class="snippet"
|
|
||||||
on:mouseenter={e => showSnippet(snippet, e.target)}
|
|
||||||
on:mouseleave={hidePopover}
|
|
||||||
on:click={() => addSnippet(snippet)}
|
|
||||||
>
|
|
||||||
{snippet.name}
|
|
||||||
<Icon
|
|
||||||
name="Edit"
|
|
||||||
hoverable
|
|
||||||
newStyles
|
|
||||||
size="S"
|
|
||||||
on:click={e => editSnippet(e, snippet)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{:else}
|
|
||||||
<div class="upgrade">
|
|
||||||
<Body size="S">
|
|
||||||
Snippets let you create reusable JS functions and values that can
|
|
||||||
all be managed in one place
|
|
||||||
</Body>
|
|
||||||
{#if enableSnippets}
|
|
||||||
<Button cta on:click={createSnippet}>Create snippet</Button>
|
|
||||||
{:else}
|
|
||||||
<UpgradeButton />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Popover
|
|
||||||
align="left-outside"
|
|
||||||
bind:this={popover}
|
|
||||||
anchor={popoverAnchor}
|
|
||||||
minWidth={0}
|
|
||||||
maxWidth={480}
|
|
||||||
maxHeight={480}
|
|
||||||
dismissible={false}
|
|
||||||
on:mouseenter={stopHidingPopover}
|
|
||||||
on:mouseleave={hidePopover}
|
|
||||||
>
|
|
||||||
<div class="snippet-popover">
|
|
||||||
{#key hoveredSnippet}
|
|
||||||
<CodeEditor
|
|
||||||
value={hoveredSnippet.code?.trim()}
|
|
||||||
mode={EditorModes.JS}
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
{/key}
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<SnippetDrawer bind:this={snippetDrawer} snippet={editableSnippet} />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.snippet-side-panel {
|
|
||||||
border-left: var(--border-light);
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
.header {
|
|
||||||
height: 53px;
|
|
||||||
padding: 0 var(--spacing-l);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: var(--border-light);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
background: var(--background);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.header :global(input) {
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.search-input,
|
|
||||||
.title {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Upgrade */
|
|
||||||
.upgrade {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-l);
|
|
||||||
}
|
|
||||||
.upgrade :global(p) {
|
|
||||||
text-align: center;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* List */
|
|
||||||
.snippet-list {
|
|
||||||
padding: 0 var(--spacing-l);
|
|
||||||
padding-bottom: var(--spacing-l);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
.snippet {
|
|
||||||
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-out, color 130ms ease-out,
|
|
||||||
border-color 130ms ease-out;
|
|
||||||
word-wrap: break-word;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.snippet:hover {
|
|
||||||
color: var(--spectrum-global-color-gray-900);
|
|
||||||
background-color: var(--spectrum-global-color-gray-50);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Popover */
|
|
||||||
.snippet-popover {
|
|
||||||
width: 400px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -8,5 +8,3 @@ export { default as DrawerBindableSlot } from "./DrawerBindableSlot.svelte"
|
||||||
export { default as EvaluationSidePanel } from "./EvaluationSidePanel.svelte"
|
export { default as EvaluationSidePanel } from "./EvaluationSidePanel.svelte"
|
||||||
export { default as ModalBindableInput } from "./ModalBindableInput.svelte"
|
export { default as ModalBindableInput } from "./ModalBindableInput.svelte"
|
||||||
export { default as ServerBindingPanel } from "./ServerBindingPanel.svelte"
|
export { default as ServerBindingPanel } from "./ServerBindingPanel.svelte"
|
||||||
export { default as SnippetDrawer } from "./SnippetDrawer.svelte"
|
|
||||||
export { default as SnippetSidePanel } from "./SnippetSidePanel.svelte"
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export * from "./sidepanel"
|
|
||||||
export * from "./codeEditor"
|
export * from "./codeEditor"
|
||||||
export * from "./errors"
|
export * from "./errors"
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
export enum SidePanel {
|
|
||||||
Bindings = "FlashOn",
|
|
||||||
Evaluation = "Play",
|
|
||||||
Snippets = "Code",
|
|
||||||
}
|
|
Loading…
Reference in New Issue