Improve snippet drawer
This commit is contained in:
parent
d38a6ed0d3
commit
7dc67185ed
|
@ -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
|
||||||
|
|
|
@ -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) ❴ ... ❵"
|
placeholder="return function(input) ❴ ... ❵"
|
||||||
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>
|
||||||
|
|
Loading…
Reference in New Issue