Add local state manipulation of snippets
This commit is contained in:
parent
706f9b5d4a
commit
84fb0dd9de
|
@ -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 {{ 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}
|
||||
|
|
|
@ -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 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);
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,3 +4,4 @@
|
|||
export { currentRole } from "./currentRole.js"
|
||||
export { dndComponentPath } from "./dndComponentPath.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 { 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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue