Add local state manipulation of snippets

This commit is contained in:
Andrew Kingston 2024-03-06 13:33:00 +00:00
parent 706f9b5d4a
commit 84fb0dd9de
7 changed files with 177 additions and 75 deletions

View File

@ -34,15 +34,17 @@
const dispatch = createEventDispatcher()
export let bindings
export let bindings = []
export let value = ""
export let valid
export let valid = true
export let allowHBS = true
export let allowJS = false
export let allowHelpers = true
export let allowSnippets = true
export let context = null
export let snippets = null
export let autofocusEditor = false
export let placeholder = null
const drawerContext = getContext("drawer")
const Modes = {
@ -55,21 +57,26 @@
Snippets: "Code",
}
let mode
let sidePanel
let initialValueJS = value?.startsWith?.("{{ js ")
let mode = initialValueJS ? Modes.JavaScript : Modes.Text
let sidePanel = SidePanels.Bindings
let getCaretPosition
let insertAtPos
let jsValue = initialValueJS ? value : null
let hbsValue = initialValueJS ? null : value
let getCaretPosition
let insertAtPos
let targetMode = null
let expressionResult
let drawerIsModal
let evaluating = false
$: drawerContext?.modal.subscribe(val => (drawerIsModal = val))
$: editorModeOptions = allowJS ? [Modes.Text, Modes.JavaScript] : [Modes.Text]
$: sidePanelOptions = getSidePanelOptions(context, allowSnippets, mode)
$: editorModeOptions = getModeOptions(allowHBS, allowJS)
$: sidePanelOptions = getSidePanelOptions(
bindings,
context,
allowSnippets,
mode
)
$: enrichedBindings = enrichBindings(bindings, context, snippets)
$: usingJS = mode === Modes.JavaScript
$: editorMode =
@ -80,15 +87,30 @@
$: requestEval(runtimeExpression, context, snippets)
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
$: {
// Ensure a valid side panel option is always selected
if (!sidePanelOptions.includes(sidePanel)) {
sidePanel = SidePanels.Bindings
sidePanel = sidePanelOptions[0]
}
}
const getSidePanelOptions = (context, allowSnippets, mode) => {
let options = [SidePanels.Bindings]
const getModeOptions = (allowHBS, allowJS) => {
let options = []
if (allowHBS) {
options.push(Modes.Text)
}
if (allowJS) {
options.push(Modes.JavaScript)
}
return options
}
const getSidePanelOptions = (bindings, context, allowSnippets, mode) => {
let options = []
if (bindings?.length) {
options.push(SidePanels.Bindings)
}
if (context) {
options.unshift(SidePanels.Evaluation)
options.push(SidePanels.Evaluation)
}
if (allowSnippets && mode === Modes.JavaScript) {
options.push(SidePanels.Snippets)
@ -197,6 +219,18 @@
}
onMount(() => {
// Set the initial mode appropriately
const initialValueMode = initialValueJS ? Modes.JavaScript : Modes.Text
if (editorModeOptions.includes(initialValueMode)) {
mode = initialValueMode
} else {
mode = editorModeOptions[0]
}
// Set the initial side panel
sidePanel = sidePanelOptions[0]
// Determine if our initial value is valid
valid = isValid(readableToRuntimeBinding(enrichedBindings, value))
})
</script>
@ -254,7 +288,8 @@
]),
]}
autofocus={autofocusEditor}
placeholder="Add bindings by typing &#123;&#123; or use the menu on the right"
placeholder={placeholder ||
"Add bindings by typing {{ or use the menu on the right"}
/>
{:else if mode === Modes.JavaScript}
<CodeEditor
@ -270,7 +305,8 @@
bind:getCaretPosition
bind:insertAtPos
autofocus={autofocusEditor}
placeholder="Add bindings by typing $ or use the menu on the right"
placeholder={placeholder ||
"Add bindings by typing $ or use the menu on the right"}
/>
{/if}
{#if targetMode}

View File

@ -0,0 +1,53 @@
<script>
import { Button, Drawer } from "@budibase/bbui"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import { decodeJSBinding, encodeJSBinding } from "@budibase/string-templates"
import { snippetStore } from "stores/builder"
export let snippet
let drawer
let code = ""
export const show = () => drawer.show()
export const hide = () => drawer.hide()
$: key = snippet?.name
$: code = snippet?.code ? encodeJSBinding(snippet.code) : ""
$: rawJS = decodeJSBinding(code)
const saveSnippet = async () => {
await snippetStore.saveSnippet({
name:
snippet?.name || "Snippet_" + Math.random().toString().substring(2, 5),
code: rawJS,
})
drawer.hide()
}
const deleteSnippet = async () => {
await snippetStore.deleteSnippet(snippet.name)
drawer.hide()
}
</script>
<Drawer title={snippet ? "Edit snippet" : "Create snippet"} bind:this={drawer}>
<svelte:fragment slot="buttons">
{#if snippet}
<Button warning on:click={deleteSnippet}>Delete</Button>
{/if}
<Button cta on:click={saveSnippet} disabled={!rawJS?.length}>Save</Button>
</svelte:fragment>
<svelte:fragment slot="body">
{#key key}
<BindingPanel
allowHBS={false}
allowJS
allowSnippets={false}
placeholder="return function(input) &#10100; ... &#10101;"
value={code}
on:change={e => (code = e.detail)}
/>
{/key}
</svelte:fragment>
</Drawer>

View File

@ -2,6 +2,7 @@
import { Input, Layout, Icon, Popover } from "@budibase/bbui"
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
import { EditorModes } from "components/common/CodeEditor"
import SnippetDrawer from "./SnippetDrawer.svelte"
export let addSnippet
export let snippets
@ -12,6 +13,8 @@
let popoverAnchor
let hoveredSnippet
let hideTimeout
let snippetDrawer
let editableSnippet
$: filteredSnippets = getFilteredSnippets(snippets, search)
@ -60,9 +63,61 @@
search = ""
}
const openSnippetModal = () => {}
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 searching}
<div class="search-input">
<Input
placeholder="Search for snippets"
autocomplete="off"
bind:value={search}
/>
</div>
<Icon size="S" name="Close" hoverable on:click={stopSearching} />
{:else}
<div class="title">Snippets</div>
<Icon size="S" name="Search" hoverable on:click={startSearching} />
<Icon size="S" name="Add" hoverable on:click={createSnippet} />
{/if}
</div>
<div class="snippet-list">
{#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
size="S"
on:click={e => editSnippet(e, snippet)}
color="var(--spectrum-global-color-gray-700)"
/>
</div>
{/each}
</div>
</Layout>
</div>
<Popover
align="left-outside"
bind:this={popover}
@ -85,39 +140,7 @@
</div>
</Popover>
<div class="snippet-side-panel">
<Layout noPadding gap="S">
<div class="header">
{#if searching}
<div class="search-input">
<Input
placeholder="Search for snippets"
autocomplete="off"
bind:value={search}
/>
</div>
<Icon size="S" name="Close" hoverable on:click={stopSearching} />
{:else}
<div class="title">Snippets</div>
<Icon size="S" name="Search" hoverable on:click={startSearching} />
<Icon size="S" name="Add" hoverable on:click={openSnippetModal} />
{/if}
</div>
<div class="snippet-list">
{#each filteredSnippets as snippet}
<div
class="snippet"
on:mouseenter={e => showSnippet(snippet, e.target)}
on:mouseleave={hidePopover}
on:click={() => addSnippet(snippet)}
>
{snippet.name}
</div>
{/each}
</div>
</Layout>
</div>
<SnippetDrawer bind:this={snippetDrawer} snippet={editableSnippet} />
<style>
.snippet-side-panel {
@ -166,6 +189,8 @@
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);

View File

@ -44,11 +44,7 @@ const createSnippetStore = () => {
store.set(metadata?.snippets || EXAMPLE_SNIPPETS)
}
const createSnippet = snippet => {
store.update(state => [...state, snippet])
}
const updateSnippet = updatedSnippet => {
const saveSnippet = updatedSnippet => {
store.update(state => [
...state.filter(snippet => snippet.name !== updatedSnippet.name),
updatedSnippet,
@ -62,8 +58,7 @@ const createSnippetStore = () => {
return {
...store,
syncMetadata,
createSnippet,
updateSnippet,
saveSnippet,
deleteSnippet,
}
}

View File

@ -4,3 +4,4 @@
export { currentRole } from "./currentRole.js"
export { dndComponentPath } from "./dndComponentPath.js"
export { devToolsEnabled } from "./devToolsEnabled.js"
export { snippets } from "./snippets.js"

View File

@ -0,0 +1,6 @@
import { derived } from "svelte/store"
import { appStore } from "../app.js"
export const snippets = derived(appStore, $appStore => $appStore.snippets)
snippets.subscribe(console.log)

View File

@ -1,28 +1,14 @@
import { Helpers } from "@budibase/bbui"
import { processString, processObjectSync } from "@budibase/string-templates"
// Regex to test inputs with to see if they are likely candidates for template strings
const looksLikeTemplate = /{{.*}}/
/**
* Enriches a given input with a row from the database.
*/
export const enrichDataBinding = async (input, context) => {
// Only accept string inputs
if (!input || typeof input !== "string") {
return input
}
// Do a fast regex check if this looks like a template string
if (!looksLikeTemplate.test(input)) {
return input
}
return processString(input, context)
}
import { processObjectSync } from "@budibase/string-templates"
import { snippets } from "../stores"
import { get } from "svelte/store"
/**
* Recursively enriches all props in a props object and returns the new props.
* Props are deeply cloned so that no mutation is done to the source object.
*/
export const enrichDataBindings = (props, context) => {
return processObjectSync(Helpers.cloneDeep(props), context, { cache: true })
const totalContext = { ...context, snippets: get(snippets) }
const opts = { cache: true }
return processObjectSync(Helpers.cloneDeep(props), totalContext, opts)
}