Add real snippet saving and fix snippet evaluation in client apps

This commit is contained in:
Andrew Kingston 2024-03-06 19:07:16 +00:00
parent 01679fbd01
commit 4d271ccb53
3 changed files with 55 additions and 60 deletions

View File

@ -6,6 +6,7 @@
Icon, Icon,
AbsTooltip, AbsTooltip,
TooltipType, TooltipType,
notifications,
} from "@budibase/bbui" } 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"
@ -22,24 +23,38 @@
let drawer let drawer
let name = "" let name = ""
let code = "" let code = ""
let loading = false
$: key = snippet?.name $: key = snippet?.name
$: name = snippet?.name || "MySnippet" $: name = snippet?.name || "MySnippet"
$: code = snippet?.code ? encodeJSBinding(snippet.code) : "" $: code = snippet?.code ? encodeJSBinding(snippet.code) : ""
$: rawJS = decodeJSBinding(code) $: rawJS = decodeJSBinding(code)
$: nameError = validateName(name) $: nameError = validateName(name, $snippetStore)
const saveSnippet = async () => { const saveSnippet = async () => {
await snippetStore.saveSnippet({ loading = true
name, try {
code: rawJS, await snippetStore.saveSnippet({
}) name,
drawer.hide() code: rawJS,
})
drawer.hide()
notifications.success(`Snippet ${name} saved`)
} catch (error) {
notifications.error("Error saving snippet")
}
loading = false
} }
const deleteSnippet = async () => { const deleteSnippet = async () => {
await snippetStore.deleteSnippet(snippet.name) loading = true
drawer.hide() try {
await snippetStore.deleteSnippet(snippet.name)
drawer.hide()
} catch (error) {
notifications.error("Error deleting snippet")
}
loading = false
} }
// Validating function names is not as easy as you think. A simple regex does // Validating function names is not as easy as you think. A simple regex does
@ -48,7 +63,7 @@
// Instead, we can run a simple regex to roughly validate it, then basically // 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 // try executing it and see if it's valid JS. The initial regex prevents
// against any potential XSS attacks here. // against any potential XSS attacks here.
const validateName = name => { const validateName = (name, snippets) => {
if (!name?.length) { if (!name?.length) {
return "Name is required" return "Name is required"
} }
@ -58,6 +73,9 @@
if (!roughValidNameRegex.test(name)) { if (!roughValidNameRegex.test(name)) {
return "No special characters or spaces" return "No special characters or spaces"
} }
if (snippets.some(snippet => snippet.name === name)) {
return "That name is already in use"
}
const js = `(function ${name}(){return true})()` const js = `(function ${name}(){return true})()`
try { try {
return eval(js) === true ? null : "Invalid name" return eval(js) === true ? null : "Invalid name"
@ -89,9 +107,11 @@
</svelte:fragment> </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} disabled={loading}>
Delete
</Button>
{/if} {/if}
<Button cta on:click={saveSnippet} disabled={nameError != null}> <Button cta on:click={saveSnippet} disabled={loading || nameError != null}>
Save Save
</Button> </Button>
</svelte:fragment> </svelte:fragment>

View File

@ -1,58 +1,33 @@
import { writable } from "svelte/store" import { writable, get } from "svelte/store"
import { API } from "api"
const EXAMPLE_SNIPPETS = [ import { appStore } from "./app"
{
name: "Square",
code: `
return function(num) {
return num * num
}
`,
},
{
name: "HelloWorld",
code: `
return "Hello, world!"
`,
},
{
name: "Colorful",
code: `
let a = null
let b = "asdasd"
let c = 123123
let d = undefined
let e = [1, 2, 3]
let f = { foo: "bar" }
let g = Math.round(1.234)
if (a === b) {
return c ?? e
}
return d || f
// comment
let h = 1 + 2 + 3 * 3
let i = true
let j = false
`,
},
]
const createSnippetStore = () => { const createSnippetStore = () => {
const store = writable(EXAMPLE_SNIPPETS) const store = writable([])
const syncMetadata = metadata => { const syncMetadata = metadata => {
store.set(metadata?.snippets || EXAMPLE_SNIPPETS) store.set(metadata?.snippets || [])
} }
const saveSnippet = updatedSnippet => { const saveSnippet = async updatedSnippet => {
store.update(state => [ const snippets = [
...state.filter(snippet => snippet.name !== updatedSnippet.name), ...get(store).filter(snippet => snippet.name !== updatedSnippet.name),
updatedSnippet, updatedSnippet,
]) ]
const app = await API.saveAppMetadata({
appId: get(appStore).appId,
metadata: { snippets },
})
syncMetadata(app)
} }
const deleteSnippet = snippetName => { const deleteSnippet = async snippetName => {
store.update(state => state.filter(snippet => snippet.name !== snippetName)) const snippets = get(store).filter(snippet => snippet.name !== snippetName)
const app = await API.saveAppMetadata({
appId: get(appStore).appId,
metadata: { snippets },
})
syncMetadata(app)
} }
return { return {

View File

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