Merge pull request #15595 from Budibase/BUDI-9068/move-snippets-to-sidepanel
Move snippets to sidepanel
This commit is contained in:
commit
dcb1f3fe0f
|
@ -64,7 +64,7 @@
|
|||
import { setContext, createEventDispatcher, onDestroy } from "svelte"
|
||||
import { generate } from "shortid"
|
||||
|
||||
export let title
|
||||
export let title = ""
|
||||
export let forceModal = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
import type { KeyboardEventHandler } from "svelte/elements"
|
||||
import { PopoverAlignment } from "../constants"
|
||||
|
||||
export let anchor: HTMLElement
|
||||
export let anchor: HTMLElement | undefined
|
||||
export let align: PopoverAlignment | `${PopoverAlignment}` =
|
||||
PopoverAlignment.Right
|
||||
export let portalTarget: string | undefined = undefined
|
||||
|
|
|
@ -27,12 +27,11 @@
|
|||
} from "../CodeEditor"
|
||||
import BindingSidePanel from "./BindingSidePanel.svelte"
|
||||
import EvaluationSidePanel from "./EvaluationSidePanel.svelte"
|
||||
import SnippetSidePanel from "./SnippetSidePanel.svelte"
|
||||
import { BindingHelpers } from "./utils"
|
||||
import { capitalise } from "@/helpers"
|
||||
import { Utils, JsonFormatter } from "@budibase/frontend-core"
|
||||
import { licensing } from "@/stores/portal"
|
||||
import { BindingMode, SidePanel } from "@budibase/types"
|
||||
import { BindingMode } from "@budibase/types"
|
||||
import type {
|
||||
EnrichedBinding,
|
||||
Snippet,
|
||||
|
@ -44,6 +43,8 @@
|
|||
import type { Log } from "@budibase/string-templates"
|
||||
import type { CodeValidator } from "@/types"
|
||||
|
||||
type SidePanel = "Bindings" | "Evaluation"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let bindings: EnrichedBinding[] = []
|
||||
|
@ -55,7 +56,7 @@
|
|||
export let context = null
|
||||
export let snippets: Snippet[] | null = null
|
||||
export let autofocusEditor = false
|
||||
export let placeholder = null
|
||||
export let placeholder: string | null = null
|
||||
export let showTabBar = true
|
||||
|
||||
let mode: BindingMode
|
||||
|
@ -71,14 +72,13 @@
|
|||
let expressionError: string | undefined
|
||||
let evaluating = false
|
||||
|
||||
$: useSnippets = allowSnippets && !$licensing.isFreePlan
|
||||
const SidePanelIcons: Record<SidePanel, string> = {
|
||||
Bindings: "FlashOn",
|
||||
Evaluation: "Play",
|
||||
}
|
||||
|
||||
$: editorModeOptions = getModeOptions(allowHBS, allowJS)
|
||||
$: sidePanelOptions = getSidePanelOptions(
|
||||
bindings,
|
||||
context,
|
||||
allowSnippets,
|
||||
mode
|
||||
)
|
||||
$: sidePanelOptions = getSidePanelOptions(bindings, context)
|
||||
$: enrichedBindings = enrichBindings(bindings, context, snippets)
|
||||
$: usingJS = mode === BindingMode.JavaScript
|
||||
$: editorMode =
|
||||
|
@ -93,7 +93,9 @@
|
|||
$: bindingOptions = bindingsToCompletions(enrichedBindings, editorMode)
|
||||
$: helperOptions = allowHelpers ? getHelperCompletions(editorMode) : []
|
||||
$: snippetsOptions =
|
||||
usingJS && useSnippets && snippets?.length ? snippets : []
|
||||
usingJS && allowSnippets && !$licensing.isFreePlan && snippets?.length
|
||||
? snippets
|
||||
: []
|
||||
|
||||
$: completions = !usingJS
|
||||
? [hbAutocomplete([...bindingOptions, ...helperOptions])]
|
||||
|
@ -137,21 +139,13 @@
|
|||
return options
|
||||
}
|
||||
|
||||
const getSidePanelOptions = (
|
||||
bindings: EnrichedBinding[],
|
||||
context: any,
|
||||
useSnippets: boolean,
|
||||
mode: BindingMode | null
|
||||
) => {
|
||||
let options = []
|
||||
const getSidePanelOptions = (bindings: EnrichedBinding[], context: any) => {
|
||||
let options: SidePanel[] = []
|
||||
if (bindings?.length) {
|
||||
options.push(SidePanel.Bindings)
|
||||
options.push("Bindings")
|
||||
}
|
||||
if (context && Object.keys(context).length > 0) {
|
||||
options.push(SidePanel.Evaluation)
|
||||
}
|
||||
if (useSnippets && mode === BindingMode.JavaScript) {
|
||||
options.push(SidePanel.Snippets)
|
||||
options.push("Evaluation")
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
@ -342,14 +336,15 @@
|
|||
{/each}
|
||||
</div>
|
||||
<div class="side-tabs">
|
||||
{#each sidePanelOptions as panel}
|
||||
{#each sidePanelOptions as panelOption}
|
||||
<ActionButton
|
||||
size="M"
|
||||
quiet
|
||||
selected={sidePanel === panel}
|
||||
on:click={() => changeSidePanel(panel)}
|
||||
selected={sidePanel === panelOption}
|
||||
on:click={() => changeSidePanel(panelOption)}
|
||||
tooltip={panelOption}
|
||||
>
|
||||
<Icon name={panel} size="S" />
|
||||
<Icon name={SidePanelIcons[panelOption]} size="S" />
|
||||
</ActionButton>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -415,16 +410,19 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="side" class:visible={!!sidePanel}>
|
||||
{#if sidePanel === SidePanel.Bindings}
|
||||
{#if sidePanel === "Bindings"}
|
||||
<BindingSidePanel
|
||||
bindings={enrichedBindings}
|
||||
{allowHelpers}
|
||||
{allowSnippets}
|
||||
{context}
|
||||
addHelper={onSelectHelper}
|
||||
addBinding={onSelectBinding}
|
||||
{addSnippet}
|
||||
{mode}
|
||||
{snippets}
|
||||
/>
|
||||
{:else if sidePanel === SidePanel.Evaluation}
|
||||
{:else if sidePanel === "Evaluation"}
|
||||
<EvaluationSidePanel
|
||||
{expressionResult}
|
||||
{expressionError}
|
||||
|
@ -432,8 +430,6 @@
|
|||
{evaluating}
|
||||
expression={editorValue ? editorValue : ""}
|
||||
/>
|
||||
{:else if sidePanel === SidePanel.Snippets}
|
||||
<SnippetSidePanel {addSnippet} {snippets} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,31 +1,52 @@
|
|||
<script lang="ts">
|
||||
import groupBy from "lodash/fp/groupBy"
|
||||
import { convertToJS } from "@budibase/string-templates"
|
||||
import { Input, Layout, Icon, Popover } from "@budibase/bbui"
|
||||
import { licensing } from "@/stores/portal"
|
||||
import {
|
||||
Input,
|
||||
Layout,
|
||||
Icon,
|
||||
Popover,
|
||||
Tags,
|
||||
Tag,
|
||||
Body,
|
||||
Button,
|
||||
} from "@budibase/bbui"
|
||||
import { handlebarsCompletions } from "@/constants/completions"
|
||||
import type { EnrichedBinding, Helper } from "@budibase/types"
|
||||
import type { EnrichedBinding, Helper, Snippet } from "@budibase/types"
|
||||
import { BindingMode } from "@budibase/types"
|
||||
import { EditorModes } from "../CodeEditor"
|
||||
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
||||
|
||||
import SnippetDrawer from "./SnippetDrawer.svelte"
|
||||
import UpgradeButton from "@/pages/builder/portal/_components/UpgradeButton.svelte"
|
||||
|
||||
export let addHelper: (_helper: Helper, _js?: boolean) => void
|
||||
export let addBinding: (_binding: EnrichedBinding) => void
|
||||
export let addSnippet: (_snippet: Snippet) => void
|
||||
export let bindings: EnrichedBinding[]
|
||||
export let snippets: Snippet[] | null
|
||||
export let mode: BindingMode
|
||||
export let allowHelpers: boolean
|
||||
export let allowSnippets: boolean
|
||||
export let context = null
|
||||
|
||||
let search = ""
|
||||
let searching = false
|
||||
let popover: Popover
|
||||
let popoverAnchor: HTMLElement | null
|
||||
let popoverAnchor: HTMLElement | undefined
|
||||
let hoverTarget: {
|
||||
helper: boolean
|
||||
type: "binding" | "helper" | "snippet"
|
||||
code: string
|
||||
description?: string
|
||||
} | null
|
||||
let helpers = handlebarsCompletions()
|
||||
let selectedCategory: string | null
|
||||
let hideTimeout: ReturnType<typeof setTimeout> | null
|
||||
let snippetDrawer: SnippetDrawer
|
||||
let editableSnippet: Snippet | null
|
||||
|
||||
$: enableSnippets = !$licensing.isFreePlan
|
||||
$: bindingIcons = bindings?.reduce<Record<string, string>>((acc, ele) => {
|
||||
if (ele.icon) {
|
||||
acc[ele.category] = acc[ele.category] || ele.icon
|
||||
|
@ -35,9 +56,14 @@
|
|||
$: categoryIcons = {
|
||||
...bindingIcons,
|
||||
Helpers: "MagicWand",
|
||||
Snippets: "Code",
|
||||
} as Record<string, string>
|
||||
$: categories = Object.entries(groupBy("category", bindings))
|
||||
$: categoryNames = getCategoryNames(categories)
|
||||
|
||||
$: categoryNames = getCategoryNames(
|
||||
categories,
|
||||
allowSnippets && mode === BindingMode.JavaScript
|
||||
)
|
||||
$: searchRgx = new RegExp(search, "ig")
|
||||
$: filteredCategories = categories
|
||||
.map(([name, categoryBindings]) => ({
|
||||
|
@ -61,6 +87,17 @@
|
|||
)
|
||||
})
|
||||
|
||||
$: filteredSnippets = getFilteredSnippets(
|
||||
enableSnippets,
|
||||
snippets || [],
|
||||
search
|
||||
)
|
||||
|
||||
function onModeChange(_mode: BindingMode) {
|
||||
selectedCategory = null
|
||||
}
|
||||
$: onModeChange(mode)
|
||||
|
||||
const getHelperExample = (helper: Helper, js: boolean) => {
|
||||
let example = helper.example || ""
|
||||
if (js) {
|
||||
|
@ -72,11 +109,17 @@
|
|||
return example || ""
|
||||
}
|
||||
|
||||
const getCategoryNames = (categories: [string, EnrichedBinding[]][]) => {
|
||||
const getCategoryNames = (
|
||||
categories: [string, EnrichedBinding[]][],
|
||||
showSnippets: boolean
|
||||
) => {
|
||||
const names = [...categories.map(cat => cat[0])]
|
||||
if (allowHelpers) {
|
||||
names.push("Helpers")
|
||||
}
|
||||
if (showSnippets) {
|
||||
names.push("Snippets")
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
|
@ -90,20 +133,20 @@
|
|||
stopHidingPopover()
|
||||
popoverAnchor = target
|
||||
hoverTarget = {
|
||||
helper: false,
|
||||
type: "binding",
|
||||
code: binding.valueHTML,
|
||||
}
|
||||
popover.show()
|
||||
}
|
||||
|
||||
const showHelperPopover = (helper: any, target: HTMLElement) => {
|
||||
const showHelperPopover = (helper: Helper, target: HTMLElement) => {
|
||||
stopHidingPopover()
|
||||
if (!helper.displayText && helper.description) {
|
||||
return
|
||||
}
|
||||
popoverAnchor = target
|
||||
hoverTarget = {
|
||||
helper: true,
|
||||
type: "helper",
|
||||
description: helper.description,
|
||||
code: getHelperExample(helper, mode === BindingMode.JavaScript),
|
||||
}
|
||||
|
@ -113,7 +156,7 @@
|
|||
const hidePopover = () => {
|
||||
hideTimeout = setTimeout(() => {
|
||||
popover.hide()
|
||||
popoverAnchor = null
|
||||
popoverAnchor = undefined
|
||||
hoverTarget = null
|
||||
hideTimeout = null
|
||||
}, 100)
|
||||
|
@ -136,10 +179,51 @@
|
|||
searching = false
|
||||
search = ""
|
||||
}
|
||||
|
||||
const getFilteredSnippets = (
|
||||
enableSnippets: boolean,
|
||||
snippets: Snippet[],
|
||||
search: string
|
||||
) => {
|
||||
if (!enableSnippets || !snippets.length) {
|
||||
return []
|
||||
}
|
||||
if (!search?.length) {
|
||||
return snippets
|
||||
}
|
||||
return snippets.filter(snippet =>
|
||||
snippet.name.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
const showSnippet = (snippet: Snippet, target: HTMLElement) => {
|
||||
stopHidingPopover()
|
||||
if (!snippet.code) {
|
||||
return
|
||||
}
|
||||
popoverAnchor = target
|
||||
hoverTarget = {
|
||||
type: "snippet",
|
||||
code: snippet.code,
|
||||
}
|
||||
|
||||
popover.show()
|
||||
}
|
||||
|
||||
const createSnippet = () => {
|
||||
editableSnippet = null
|
||||
snippetDrawer.show()
|
||||
}
|
||||
|
||||
const editSnippet = (e: Event, snippet: Snippet) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
editableSnippet = snippet
|
||||
snippetDrawer.show()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if popoverAnchor && hoverTarget}
|
||||
<Popover
|
||||
<Popover
|
||||
align="left-outside"
|
||||
bind:this={popover}
|
||||
anchor={popoverAnchor}
|
||||
|
@ -149,8 +233,12 @@
|
|||
dismissible={false}
|
||||
on:mouseenter={stopHidingPopover}
|
||||
on:mouseleave={hidePopover}
|
||||
>
|
||||
{#if hoverTarget}
|
||||
<div
|
||||
class="binding-popover"
|
||||
class:has-code={hoverTarget.type !== "binding"}
|
||||
>
|
||||
<div class="binding-popover" class:helper={hoverTarget.helper}>
|
||||
{#if hoverTarget.description}
|
||||
<div>
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||
|
@ -158,12 +246,20 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#if hoverTarget.code}
|
||||
{#if mode === BindingMode.JavaScript}
|
||||
<CodeEditor
|
||||
value={hoverTarget.code?.trim()}
|
||||
mode={EditorModes.JS}
|
||||
readonly
|
||||
/>
|
||||
{:else if mode === BindingMode.Text}
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||
<pre>{@html hoverTarget.code}</pre>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
{/if}
|
||||
</Popover>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
|
@ -178,6 +274,25 @@
|
|||
on:click={() => (selectedCategory = null)}
|
||||
/>
|
||||
{selectedCategory}
|
||||
{#if selectedCategory === "Snippets"}
|
||||
{#if enableSnippets}
|
||||
<div class="add-snippet-button">
|
||||
<Icon
|
||||
size="S"
|
||||
name="Add"
|
||||
hoverable
|
||||
newStyles
|
||||
on:click={createSnippet}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="title">
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Premium</Tag>
|
||||
</Tags>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -281,7 +396,6 @@
|
|||
class="binding"
|
||||
on:mouseenter={e =>
|
||||
showHelperPopover(helper, e.currentTarget)}
|
||||
on:mouseleave={hidePopover}
|
||||
on:click={() =>
|
||||
addHelper(helper, mode === BindingMode.JavaScript)}
|
||||
>
|
||||
|
@ -295,10 +409,48 @@
|
|||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if selectedCategory === "Snippets" || search}
|
||||
<div class="snippet-list">
|
||||
{#if enableSnippets && filteredSnippets.length}
|
||||
{#each filteredSnippets as snippet}
|
||||
<li
|
||||
class="snippet"
|
||||
on:mouseenter={e => showSnippet(snippet, e.currentTarget)}
|
||||
on:mouseleave={hidePopover}
|
||||
on:click={() => addSnippet(snippet)}
|
||||
>
|
||||
{snippet.name}
|
||||
<Icon
|
||||
name="Edit"
|
||||
hoverable
|
||||
newStyles
|
||||
size="S"
|
||||
on:click={e => editSnippet(e, snippet)}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
{:else if !search}
|
||||
<div class="upgrade">
|
||||
<Body size="S">
|
||||
Snippets let you create reusable JS functions and values that
|
||||
can all be managed in one place
|
||||
</Body>
|
||||
{#if enableSnippets}
|
||||
<Button cta on:click={createSnippet}>Create snippet</Button>
|
||||
{:else}
|
||||
<UpgradeButton />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<SnippetDrawer bind:this={snippetDrawer} snippet={editableSnippet} />
|
||||
|
||||
<style>
|
||||
.binding-side-panel {
|
||||
border-left: var(--border-light);
|
||||
|
@ -363,6 +515,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
justify-content: space-between;
|
||||
}
|
||||
li.binding .binding__typeWrap {
|
||||
flex: 1;
|
||||
|
@ -438,7 +591,7 @@
|
|||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.binding-popover.helper pre {
|
||||
.binding-popover.has-code pre {
|
||||
color: var(--spectrum-global-color-blue-700);
|
||||
}
|
||||
.binding-popover pre :global(span) {
|
||||
|
@ -450,7 +603,50 @@
|
|||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.binding-popover.helper :global(code) {
|
||||
.binding-popover.has-code :global(code) {
|
||||
font-size: 12px;
|
||||
}
|
||||
.binding-popover.has-code :global(.cm-line),
|
||||
.binding-popover.has-code :global(.cm-content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Snippets */
|
||||
.add-snippet-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
.snippet-list {
|
||||
padding: 0 var(--spacing-l);
|
||||
padding-bottom: var(--spacing-l);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.snippet {
|
||||
font-size: var(--font-size-s);
|
||||
padding: var(--spacing-m);
|
||||
border-radius: 4px;
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
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);
|
||||
background-color: var(--spectrum-global-color-gray-50);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Upgrade */
|
||||
.upgrade {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
.upgrade :global(p) {
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
|
@ -14,19 +14,27 @@
|
|||
import { getSequentialName } from "@/helpers/duplicate"
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||
import { ValidSnippetNameRegex } from "@budibase/shared-core"
|
||||
import type { Snippet } from "@budibase/types"
|
||||
|
||||
export let snippet
|
||||
|
||||
export const show = () => drawer.show()
|
||||
export const show = () => {
|
||||
if (!snippet) {
|
||||
key = Math.random().toString()
|
||||
// Reset state when creating multiple snippets
|
||||
code = ""
|
||||
name = defaultName
|
||||
}
|
||||
drawer.show()
|
||||
}
|
||||
export const hide = () => drawer.hide()
|
||||
export let snippet: Snippet | null
|
||||
|
||||
const firstCharNumberRegex = /^[0-9].*$/
|
||||
|
||||
let drawer
|
||||
let drawer: Drawer
|
||||
let name = ""
|
||||
let code = ""
|
||||
let loading = false
|
||||
let deleteConfirmationDialog
|
||||
let deleteConfirmationDialog: ConfirmDialog
|
||||
|
||||
$: defaultName = getSequentialName($snippets, "MySnippet", {
|
||||
getName: x => x.name,
|
||||
|
@ -40,11 +48,11 @@
|
|||
const saveSnippet = async () => {
|
||||
loading = true
|
||||
try {
|
||||
const newSnippet = { name, code: rawJS }
|
||||
const newSnippet: Snippet = { name, code: rawJS || "" }
|
||||
await snippets.saveSnippet(newSnippet)
|
||||
drawer.hide()
|
||||
notifications.success(`Snippet ${newSnippet.name} saved`)
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
notifications.error(error.message || "Error saving snippet")
|
||||
}
|
||||
loading = false
|
||||
|
@ -53,7 +61,9 @@
|
|||
const deleteSnippet = async () => {
|
||||
loading = true
|
||||
try {
|
||||
if (snippet) {
|
||||
await snippets.deleteSnippet(snippet.name)
|
||||
}
|
||||
drawer.hide()
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting snippet")
|
||||
|
@ -61,7 +71,7 @@
|
|||
loading = false
|
||||
}
|
||||
|
||||
const validateName = (name, snippets) => {
|
||||
const validateName = (name: string, snippets: Snippet[]) => {
|
||||
if (!name?.length) {
|
||||
return "Name is required"
|
||||
}
|
||||
|
@ -108,7 +118,11 @@
|
|||
Delete
|
||||
</Button>
|
||||
{/if}
|
||||
<Button cta on:click={saveSnippet} disabled={!code || loading || nameError}>
|
||||
<Button
|
||||
cta
|
||||
on:click={saveSnippet}
|
||||
disabled={!code || loading || !!nameError}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
|
@ -124,9 +138,7 @@
|
|||
value={code}
|
||||
on:change={e => (code = e.detail)}
|
||||
>
|
||||
<div slot="tabs">
|
||||
<Input placeholder="Name" />
|
||||
</div>
|
||||
</BindingPanel>
|
||||
{/key}
|
||||
</svelte:fragment>
|
||||
|
|
|
@ -1,278 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Input,
|
||||
Layout,
|
||||
Icon,
|
||||
Popover,
|
||||
Tags,
|
||||
Tag,
|
||||
Body,
|
||||
Button,
|
||||
} from "@budibase/bbui"
|
||||
import CodeEditor from "@/components/common/CodeEditor/CodeEditor.svelte"
|
||||
import { EditorModes } from "@/components/common/CodeEditor"
|
||||
import SnippetDrawer from "./SnippetDrawer.svelte"
|
||||
import { licensing } from "@/stores/portal"
|
||||
import UpgradeButton from "@/pages/builder/portal/_components/UpgradeButton.svelte"
|
||||
|
||||
export let addSnippet
|
||||
export let snippets
|
||||
|
||||
let search = ""
|
||||
let searching = false
|
||||
let popover
|
||||
let popoverAnchor
|
||||
let hoveredSnippet
|
||||
let hideTimeout
|
||||
let snippetDrawer
|
||||
let editableSnippet
|
||||
|
||||
$: enableSnippets = !$licensing.isFreePlan
|
||||
$: filteredSnippets = getFilteredSnippets(enableSnippets, snippets, search)
|
||||
|
||||
const getFilteredSnippets = (enableSnippets, snippets, search) => {
|
||||
if (!enableSnippets || !snippets?.length) {
|
||||
return []
|
||||
}
|
||||
if (!search?.length) {
|
||||
return snippets
|
||||
}
|
||||
return snippets.filter(snippet =>
|
||||
snippet.name.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
const showSnippet = (snippet, target) => {
|
||||
stopHidingPopover()
|
||||
popoverAnchor = target
|
||||
hoveredSnippet = snippet
|
||||
popover.show()
|
||||
}
|
||||
|
||||
const hidePopover = () => {
|
||||
hideTimeout = setTimeout(() => {
|
||||
popover.hide()
|
||||
popoverAnchor = null
|
||||
hoveredSnippet = null
|
||||
hideTimeout = null
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const stopHidingPopover = () => {
|
||||
if (hideTimeout) {
|
||||
clearTimeout(hideTimeout)
|
||||
hideTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
const startSearching = () => {
|
||||
searching = true
|
||||
search = ""
|
||||
}
|
||||
|
||||
const stopSearching = () => {
|
||||
searching = false
|
||||
search = ""
|
||||
}
|
||||
|
||||
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 enableSnippets}
|
||||
{#if searching}
|
||||
<div class="search-input">
|
||||
<Input
|
||||
placeholder="Search for snippets"
|
||||
autocomplete="off"
|
||||
bind:value={search}
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
<Icon
|
||||
size="S"
|
||||
name="Close"
|
||||
hoverable
|
||||
newStyles
|
||||
on:click={stopSearching}
|
||||
/>
|
||||
{:else}
|
||||
<div class="title">Snippets</div>
|
||||
<Icon
|
||||
size="S"
|
||||
name="Search"
|
||||
hoverable
|
||||
newStyles
|
||||
on:click={startSearching}
|
||||
/>
|
||||
<Icon
|
||||
size="S"
|
||||
name="Add"
|
||||
hoverable
|
||||
newStyles
|
||||
on:click={createSnippet}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="title">
|
||||
Snippets
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Premium</Tag>
|
||||
</Tags>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="snippet-list">
|
||||
{#if enableSnippets && filteredSnippets?.length}
|
||||
{#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
|
||||
newStyles
|
||||
size="S"
|
||||
on:click={e => editSnippet(e, snippet)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="upgrade">
|
||||
<Body size="S">
|
||||
Snippets let you create reusable JS functions and values that can
|
||||
all be managed in one place
|
||||
</Body>
|
||||
{#if enableSnippets}
|
||||
<Button cta on:click={createSnippet}>Create snippet</Button>
|
||||
{:else}
|
||||
<UpgradeButton />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<Popover
|
||||
align="left-outside"
|
||||
bind:this={popover}
|
||||
anchor={popoverAnchor}
|
||||
minWidth={0}
|
||||
maxWidth={480}
|
||||
maxHeight={480}
|
||||
dismissible={false}
|
||||
on:mouseenter={stopHidingPopover}
|
||||
on:mouseleave={hidePopover}
|
||||
>
|
||||
<div class="snippet-popover">
|
||||
{#key hoveredSnippet}
|
||||
<CodeEditor
|
||||
value={hoveredSnippet.code?.trim()}
|
||||
mode={EditorModes.JS}
|
||||
readonly
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<SnippetDrawer bind:this={snippetDrawer} snippet={editableSnippet} />
|
||||
|
||||
<style>
|
||||
.snippet-side-panel {
|
||||
border-left: var(--border-light);
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
height: 53px;
|
||||
padding: 0 var(--spacing-l);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: var(--border-light);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
gap: var(--spacing-m);
|
||||
background: var(--background);
|
||||
z-index: 1;
|
||||
}
|
||||
.header :global(input) {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.search-input,
|
||||
.title {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
/* Upgrade */
|
||||
.upgrade {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
.upgrade :global(p) {
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* List */
|
||||
.snippet-list {
|
||||
padding: 0 var(--spacing-l);
|
||||
padding-bottom: var(--spacing-l);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
.snippet {
|
||||
font-size: var(--font-size-s);
|
||||
padding: var(--spacing-m);
|
||||
border-radius: 4px;
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
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);
|
||||
background-color: var(--spectrum-global-color-gray-50);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Popover */
|
||||
.snippet-popover {
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
|
@ -8,5 +8,3 @@ export { default as DrawerBindableSlot } from "./DrawerBindableSlot.svelte"
|
|||
export { default as EvaluationSidePanel } from "./EvaluationSidePanel.svelte"
|
||||
export { default as ModalBindableInput } from "./ModalBindableInput.svelte"
|
||||
export { default as ServerBindingPanel } from "./ServerBindingPanel.svelte"
|
||||
export { default as SnippetDrawer } from "./SnippetDrawer.svelte"
|
||||
export { default as SnippetSidePanel } from "./SnippetSidePanel.svelte"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
export * from "./sidepanel"
|
||||
export * from "./codeEditor"
|
||||
export * from "./errors"
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
export enum SidePanel {
|
||||
Bindings = "FlashOn",
|
||||
Evaluation = "Play",
|
||||
Snippets = "Code",
|
||||
}
|
Loading…
Reference in New Issue