Improve snippet drawer

This commit is contained in:
Andrew Kingston 2024-03-06 14:33:17 +00:00
parent d38a6ed0d3
commit 7dc67185ed
2 changed files with 95 additions and 26 deletions

View File

@ -7,7 +7,7 @@
Body, Body,
Button, Button,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createEventDispatcher, getContext, onMount } from "svelte" import { createEventDispatcher, onMount } from "svelte"
import { import {
isValid, isValid,
decodeJSBinding, decodeJSBinding,
@ -44,6 +44,7 @@
export let snippets = null export let snippets = null
export let autofocusEditor = false export let autofocusEditor = false
export let placeholder = null export let placeholder = null
export let showTabBar = true
const Modes = { const Modes = {
Text: "Text", Text: "Text",
@ -234,10 +235,8 @@
<DrawerContent padding={false}> <DrawerContent padding={false}>
<div class="binding-panel"> <div class="binding-panel">
<div class="main"> <div class="main">
<div class="tabs"> {#if showTabBar}
{#if $$slots.tabs} <div class="tabs">
<slot name="tabs" />
{:else}
<div class="editor-tabs"> <div class="editor-tabs">
{#each editorModeOptions as editorMode} {#each editorModeOptions as editorMode}
<ActionButton <ActionButton
@ -250,20 +249,20 @@
</ActionButton> </ActionButton>
{/each} {/each}
</div> </div>
{/if} <div class="side-tabs">
<div class="side-tabs"> {#each sidePanelOptions as panel}
{#each sidePanelOptions as panel} <ActionButton
<ActionButton size="M"
size="M" quiet
quiet selected={sidePanel === panel}
selected={sidePanel === panel} on:click={() => changeSidePanel(panel)}
on:click={() => changeSidePanel(panel)} >
> <Icon name={panel} size="S" />
<Icon name={panel} size="S" /> </ActionButton>
</ActionButton> {/each}
{/each} </div>
</div> </div>
</div> {/if}
<div class="editor"> <div class="editor">
{#if mode === Modes.Text} {#if mode === Modes.Text}
<CodeEditor <CodeEditor

View File

@ -1,25 +1,36 @@
<script> <script>
import { Button, Drawer, Input } from "@budibase/bbui" import {
Button,
Drawer,
Input,
Icon,
AbsTooltip,
TooltipType,
} from "@budibase/bbui"
import BindingPanel from "components/common/bindings/BindingPanel.svelte" import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import { decodeJSBinding, encodeJSBinding } from "@budibase/string-templates" import { decodeJSBinding, encodeJSBinding } from "@budibase/string-templates"
import { snippetStore } from "stores/builder" import { snippetStore } from "stores/builder"
export let snippet export let snippet
let drawer
let code = ""
export const show = () => drawer.show() export const show = () => drawer.show()
export const hide = () => drawer.hide() export const hide = () => drawer.hide()
const roughValidNameRegex = /^[_$A-Z\xA0-\uFFFF][_$A-Z0-9\xA0-\uFFFF]*$/i
let drawer
let name = ""
let code = ""
$: key = snippet?.name $: key = snippet?.name
$: name = snippet?.name || "MySnippet"
$: code = snippet?.code ? encodeJSBinding(snippet.code) : "" $: code = snippet?.code ? encodeJSBinding(snippet.code) : ""
$: rawJS = decodeJSBinding(code) $: rawJS = decodeJSBinding(code)
$: nameValid = validateName(name)
const saveSnippet = async () => { const saveSnippet = async () => {
await snippetStore.saveSnippet({ await snippetStore.saveSnippet({
name: name,
snippet?.name || "Snippet_" + Math.random().toString().substring(2, 5),
code: rawJS, code: rawJS,
}) })
drawer.hide() drawer.hide()
@ -29,14 +40,56 @@
await snippetStore.deleteSnippet(snippet.name) await snippetStore.deleteSnippet(snippet.name)
drawer.hide() drawer.hide()
} }
// Validating function names is not as easy as you think. A simple regex does
// not work, as there are a bunch of reserved words. The correct regex for
// this is about 12K characters long.
// Instead, we can run a simple regex to roughly validate it, then basically
// try executing it and see if it's valid JS. The initial regex prevents
// against any potential XSS attacks here.
const validateName = name => {
if (!roughValidNameRegex.test(name)) {
return false
}
const js = `(function ${name}(){return true})()`
try {
return eval(js) === true
} catch (error) {
return false
}
}
</script> </script>
<Drawer title={snippet ? "Edit snippet" : "Create snippet"} bind:this={drawer}> <Drawer bind:this={drawer}>
<svelte:fragment slot="title">
{#if snippet}
{snippet.name}
{:else}
<div class="name" class:invalid={!nameValid}>
<span>Name</span>
<Input bind:value={name} />
{#if !nameValid}
<AbsTooltip
text="Alphanumeric characters only, with no spaces"
type={TooltipType.Negative}
>
<Icon
name="Help"
size="S"
color="var(--spectrum-global-color-red-400)"
/>
</AbsTooltip>
{/if}
</div>
{/if}
</svelte:fragment>
<svelte:fragment slot="buttons"> <svelte:fragment slot="buttons">
{#if snippet} {#if snippet}
<Button warning on:click={deleteSnippet}>Delete</Button> <Button warning on:click={deleteSnippet}>Delete</Button>
{/if} {/if}
<Button cta on:click={saveSnippet} disabled={!rawJS?.length}>Save</Button> <Button cta on:click={saveSnippet} disabled={!rawJS?.length || !nameValid}>
Save
</Button>
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="body"> <svelte:fragment slot="body">
{#key key} {#key key}
@ -44,6 +97,7 @@
allowHBS={false} allowHBS={false}
allowJS allowJS
allowSnippets={false} allowSnippets={false}
showTabBar={false}
placeholder="return function(input) &#10100; ... &#10101;" placeholder="return function(input) &#10100; ... &#10101;"
value={code} value={code}
on:change={e => (code = e.detail)} on:change={e => (code = e.detail)}
@ -55,3 +109,19 @@
{/key} {/key}
</svelte:fragment> </svelte:fragment>
</Drawer> </Drawer>
<style>
.name {
display: flex;
gap: var(--spacing-l);
align-items: center;
position: relative;
}
.name.invalid :global(input) {
width: 200px;
}
.name :global(.icon) {
position: absolute;
right: 10px;
}
</style>