Add local state manipulation of snippets
This commit is contained in:
parent
706f9b5d4a
commit
84fb0dd9de
|
@ -34,15 +34,17 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let bindings
|
export let bindings = []
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let valid
|
export let valid = true
|
||||||
|
export let allowHBS = true
|
||||||
export let allowJS = false
|
export let allowJS = false
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let allowSnippets = true
|
export let allowSnippets = true
|
||||||
export let context = null
|
export let context = null
|
||||||
export let snippets = null
|
export let snippets = null
|
||||||
export let autofocusEditor = false
|
export let autofocusEditor = false
|
||||||
|
export let placeholder = null
|
||||||
|
|
||||||
const drawerContext = getContext("drawer")
|
const drawerContext = getContext("drawer")
|
||||||
const Modes = {
|
const Modes = {
|
||||||
|
@ -55,21 +57,26 @@
|
||||||
Snippets: "Code",
|
Snippets: "Code",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mode
|
||||||
|
let sidePanel
|
||||||
let initialValueJS = value?.startsWith?.("{{ js ")
|
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 jsValue = initialValueJS ? value : null
|
||||||
let hbsValue = initialValueJS ? null : value
|
let hbsValue = initialValueJS ? null : value
|
||||||
|
let getCaretPosition
|
||||||
|
let insertAtPos
|
||||||
let targetMode = null
|
let targetMode = null
|
||||||
let expressionResult
|
let expressionResult
|
||||||
let drawerIsModal
|
let drawerIsModal
|
||||||
let evaluating = false
|
let evaluating = false
|
||||||
|
|
||||||
$: drawerContext?.modal.subscribe(val => (drawerIsModal = val))
|
$: drawerContext?.modal.subscribe(val => (drawerIsModal = val))
|
||||||
$: editorModeOptions = allowJS ? [Modes.Text, Modes.JavaScript] : [Modes.Text]
|
$: editorModeOptions = getModeOptions(allowHBS, allowJS)
|
||||||
$: sidePanelOptions = getSidePanelOptions(context, allowSnippets, mode)
|
$: sidePanelOptions = getSidePanelOptions(
|
||||||
|
bindings,
|
||||||
|
context,
|
||||||
|
allowSnippets,
|
||||||
|
mode
|
||||||
|
)
|
||||||
$: enrichedBindings = enrichBindings(bindings, context, snippets)
|
$: enrichedBindings = enrichBindings(bindings, context, snippets)
|
||||||
$: usingJS = mode === Modes.JavaScript
|
$: usingJS = mode === Modes.JavaScript
|
||||||
$: editorMode =
|
$: editorMode =
|
||||||
|
@ -80,15 +87,30 @@
|
||||||
$: requestEval(runtimeExpression, context, snippets)
|
$: requestEval(runtimeExpression, context, snippets)
|
||||||
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
|
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
|
||||||
$: {
|
$: {
|
||||||
|
// Ensure a valid side panel option is always selected
|
||||||
if (!sidePanelOptions.includes(sidePanel)) {
|
if (!sidePanelOptions.includes(sidePanel)) {
|
||||||
sidePanel = SidePanels.Bindings
|
sidePanel = sidePanelOptions[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSidePanelOptions = (context, allowSnippets, mode) => {
|
const getModeOptions = (allowHBS, allowJS) => {
|
||||||
let options = [SidePanels.Bindings]
|
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) {
|
if (context) {
|
||||||
options.unshift(SidePanels.Evaluation)
|
options.push(SidePanels.Evaluation)
|
||||||
}
|
}
|
||||||
if (allowSnippets && mode === Modes.JavaScript) {
|
if (allowSnippets && mode === Modes.JavaScript) {
|
||||||
options.push(SidePanels.Snippets)
|
options.push(SidePanels.Snippets)
|
||||||
|
@ -197,6 +219,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
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))
|
valid = isValid(readableToRuntimeBinding(enrichedBindings, value))
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -254,7 +288,8 @@
|
||||||
]),
|
]),
|
||||||
]}
|
]}
|
||||||
autofocus={autofocusEditor}
|
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"}
|
||||||
/>
|
/>
|
||||||
{:else if mode === Modes.JavaScript}
|
{:else if mode === Modes.JavaScript}
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
@ -270,7 +305,8 @@
|
||||||
bind:getCaretPosition
|
bind:getCaretPosition
|
||||||
bind:insertAtPos
|
bind:insertAtPos
|
||||||
autofocus={autofocusEditor}
|
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}
|
||||||
{#if targetMode}
|
{#if targetMode}
|
||||||
|
|
|
@ -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) ❴ ... ❵"
|
||||||
|
value={code}
|
||||||
|
on:change={e => (code = e.detail)}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</svelte:fragment>
|
||||||
|
</Drawer>
|
|
@ -2,6 +2,7 @@
|
||||||
import { Input, Layout, Icon, Popover } from "@budibase/bbui"
|
import { Input, Layout, Icon, Popover } from "@budibase/bbui"
|
||||||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||||
import { EditorModes } from "components/common/CodeEditor"
|
import { EditorModes } from "components/common/CodeEditor"
|
||||||
|
import SnippetDrawer from "./SnippetDrawer.svelte"
|
||||||
|
|
||||||
export let addSnippet
|
export let addSnippet
|
||||||
export let snippets
|
export let snippets
|
||||||
|
@ -12,6 +13,8 @@
|
||||||
let popoverAnchor
|
let popoverAnchor
|
||||||
let hoveredSnippet
|
let hoveredSnippet
|
||||||
let hideTimeout
|
let hideTimeout
|
||||||
|
let snippetDrawer
|
||||||
|
let editableSnippet
|
||||||
|
|
||||||
$: filteredSnippets = getFilteredSnippets(snippets, search)
|
$: filteredSnippets = getFilteredSnippets(snippets, search)
|
||||||
|
|
||||||
|
@ -60,9 +63,61 @@
|
||||||
search = ""
|
search = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const openSnippetModal = () => {}
|
const createSnippet = () => {
|
||||||
|
editableSnippet = null
|
||||||
|
snippetDrawer.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const editSnippet = (e, snippet) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
editableSnippet = snippet
|
||||||
|
snippetDrawer.show()
|
||||||
|
}
|
||||||
</script>
|
</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
|
<Popover
|
||||||
align="left-outside"
|
align="left-outside"
|
||||||
bind:this={popover}
|
bind:this={popover}
|
||||||
|
@ -85,39 +140,7 @@
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<div class="snippet-side-panel">
|
<SnippetDrawer bind:this={snippetDrawer} snippet={editableSnippet} />
|
||||||
<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>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.snippet-side-panel {
|
.snippet-side-panel {
|
||||||
|
@ -166,6 +189,8 @@
|
||||||
transition: background-color 130ms ease-out, color 130ms ease-out,
|
transition: background-color 130ms ease-out, color 130ms ease-out,
|
||||||
border-color 130ms ease-out;
|
border-color 130ms ease-out;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
.snippet:hover {
|
.snippet:hover {
|
||||||
color: var(--spectrum-global-color-gray-900);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
|
|
@ -44,11 +44,7 @@ const createSnippetStore = () => {
|
||||||
store.set(metadata?.snippets || EXAMPLE_SNIPPETS)
|
store.set(metadata?.snippets || EXAMPLE_SNIPPETS)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createSnippet = snippet => {
|
const saveSnippet = updatedSnippet => {
|
||||||
store.update(state => [...state, snippet])
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSnippet = updatedSnippet => {
|
|
||||||
store.update(state => [
|
store.update(state => [
|
||||||
...state.filter(snippet => snippet.name !== updatedSnippet.name),
|
...state.filter(snippet => snippet.name !== updatedSnippet.name),
|
||||||
updatedSnippet,
|
updatedSnippet,
|
||||||
|
@ -62,8 +58,7 @@ const createSnippetStore = () => {
|
||||||
return {
|
return {
|
||||||
...store,
|
...store,
|
||||||
syncMetadata,
|
syncMetadata,
|
||||||
createSnippet,
|
saveSnippet,
|
||||||
updateSnippet,
|
|
||||||
deleteSnippet,
|
deleteSnippet,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,4 @@
|
||||||
export { currentRole } from "./currentRole.js"
|
export { currentRole } from "./currentRole.js"
|
||||||
export { dndComponentPath } from "./dndComponentPath.js"
|
export { dndComponentPath } from "./dndComponentPath.js"
|
||||||
export { devToolsEnabled } from "./devToolsEnabled.js"
|
export { devToolsEnabled } from "./devToolsEnabled.js"
|
||||||
|
export { snippets } from "./snippets.js"
|
||||||
|
|
|
@ -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)
|
|
@ -1,28 +1,14 @@
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { processString, processObjectSync } from "@budibase/string-templates"
|
import { processObjectSync } from "@budibase/string-templates"
|
||||||
|
import { snippets } from "../stores"
|
||||||
// Regex to test inputs with to see if they are likely candidates for template strings
|
import { get } from "svelte/store"
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively enriches all props in a props object and returns the new props.
|
* 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.
|
* Props are deeply cloned so that no mutation is done to the source object.
|
||||||
*/
|
*/
|
||||||
export const enrichDataBindings = (props, context) => {
|
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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue