Unify
This commit is contained in:
parent
ed2d52da39
commit
39ff078978
|
@ -18,7 +18,7 @@
|
||||||
import type { KeyboardEventHandler } from "svelte/elements"
|
import type { KeyboardEventHandler } from "svelte/elements"
|
||||||
import { PopoverAlignment } from "../constants"
|
import { PopoverAlignment } from "../constants"
|
||||||
|
|
||||||
export let anchor: HTMLElement
|
export let anchor: HTMLElement | undefined
|
||||||
export let align: PopoverAlignment | `${PopoverAlignment}` =
|
export let align: PopoverAlignment | `${PopoverAlignment}` =
|
||||||
PopoverAlignment.Right
|
PopoverAlignment.Right
|
||||||
export let portalTarget: string | undefined = undefined
|
export let portalTarget: string | undefined = undefined
|
||||||
|
|
|
@ -1,14 +1,25 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import groupBy from "lodash/fp/groupBy"
|
import groupBy from "lodash/fp/groupBy"
|
||||||
import { convertToJS } from "@budibase/string-templates"
|
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 { handlebarsCompletions } from "@/constants/completions"
|
||||||
import type { EnrichedBinding, Helper, Snippet } from "@budibase/types"
|
import type { EnrichedBinding, Helper, Snippet } from "@budibase/types"
|
||||||
import { BindingMode } from "@budibase/types"
|
import { BindingMode } from "@budibase/types"
|
||||||
import { EditorModes } from "../CodeEditor"
|
import { EditorModes } from "../CodeEditor"
|
||||||
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
||||||
import SnippetsSidePanelHeader from "./snippets/SidePanelHeader.svelte"
|
|
||||||
import SnippetsSidePanelContent from "./snippets/SidePanelContent.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 addHelper: (_helper: Helper, _js?: boolean) => void
|
||||||
export let addBinding: (_binding: EnrichedBinding) => void
|
export let addBinding: (_binding: EnrichedBinding) => void
|
||||||
|
@ -23,16 +34,19 @@
|
||||||
let search = ""
|
let search = ""
|
||||||
let searching = false
|
let searching = false
|
||||||
let popover: Popover
|
let popover: Popover
|
||||||
let popoverAnchor: HTMLElement | null
|
let popoverAnchor: HTMLElement | undefined
|
||||||
let hoverTarget: {
|
let hoverTarget: {
|
||||||
type: "binding" | "helper"
|
type: "binding" | "helper" | "snippet"
|
||||||
code: string
|
code: string
|
||||||
description?: string
|
description?: string
|
||||||
} | null
|
} | null
|
||||||
let helpers = handlebarsCompletions()
|
let helpers = handlebarsCompletions()
|
||||||
let selectedCategory: string | null
|
let selectedCategory: string | null
|
||||||
let hideTimeout: ReturnType<typeof setTimeout> | 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) => {
|
$: bindingIcons = bindings?.reduce<Record<string, string>>((acc, ele) => {
|
||||||
if (ele.icon) {
|
if (ele.icon) {
|
||||||
acc[ele.category] = acc[ele.category] || ele.icon
|
acc[ele.category] = acc[ele.category] || ele.icon
|
||||||
|
@ -73,6 +87,12 @@
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$: filteredSnippets = getFilteredSnippets(
|
||||||
|
enableSnippets,
|
||||||
|
snippets || [],
|
||||||
|
search
|
||||||
|
)
|
||||||
|
|
||||||
function onModeChange(_mode: BindingMode) {
|
function onModeChange(_mode: BindingMode) {
|
||||||
selectedCategory = null
|
selectedCategory = null
|
||||||
}
|
}
|
||||||
|
@ -136,7 +156,7 @@
|
||||||
const hidePopover = () => {
|
const hidePopover = () => {
|
||||||
hideTimeout = setTimeout(() => {
|
hideTimeout = setTimeout(() => {
|
||||||
popover.hide()
|
popover.hide()
|
||||||
popoverAnchor = null
|
popoverAnchor = undefined
|
||||||
hoverTarget = null
|
hoverTarget = null
|
||||||
hideTimeout = null
|
hideTimeout = null
|
||||||
}, 100)
|
}, 100)
|
||||||
|
@ -159,20 +179,62 @@
|
||||||
searching = false
|
searching = false
|
||||||
search = ""
|
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>
|
</script>
|
||||||
|
|
||||||
{#if popoverAnchor && hoverTarget}
|
<Popover
|
||||||
<Popover
|
align="left-outside"
|
||||||
align="left-outside"
|
bind:this={popover}
|
||||||
bind:this={popover}
|
anchor={popoverAnchor}
|
||||||
anchor={popoverAnchor}
|
minWidth={0}
|
||||||
minWidth={0}
|
maxWidth={480}
|
||||||
maxWidth={480}
|
maxHeight={480}
|
||||||
maxHeight={480}
|
dismissible={false}
|
||||||
dismissible={false}
|
on:mouseenter={stopHidingPopover}
|
||||||
on:mouseenter={stopHidingPopover}
|
on:mouseleave={hidePopover}
|
||||||
on:mouseleave={hidePopover}
|
>
|
||||||
>
|
{#if hoverTarget}
|
||||||
<div
|
<div
|
||||||
class="binding-popover"
|
class="binding-popover"
|
||||||
class:has-code={hoverTarget.type !== "binding"}
|
class:has-code={hoverTarget.type !== "binding"}
|
||||||
|
@ -196,8 +258,8 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
{/if}
|
||||||
{/if}
|
</Popover>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
|
@ -213,7 +275,23 @@
|
||||||
/>
|
/>
|
||||||
{selectedCategory}
|
{selectedCategory}
|
||||||
{#if selectedCategory === "Snippets"}
|
{#if selectedCategory === "Snippets"}
|
||||||
<SnippetsSidePanelHeader />
|
{#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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -333,12 +411,46 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if selectedCategory === "Snippets" || search}
|
{#if selectedCategory === "Snippets" || search}
|
||||||
<SnippetsSidePanelContent {snippets} {addSnippet} {search} />
|
<div class="snippet-list">
|
||||||
|
{#if enableSnippets}
|
||||||
|
{#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}
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SnippetDrawer bind:this={snippetDrawer} snippet={editableSnippet} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.binding-side-panel {
|
.binding-side-panel {
|
||||||
border-left: var(--border-light);
|
border-left: var(--border-light);
|
||||||
|
@ -498,4 +610,32 @@
|
||||||
.binding-popover.has-code :global(.cm-content) {
|
.binding-popover.has-code :global(.cm-content) {
|
||||||
padding: 0;
|
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;
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,191 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Icon, Popover, 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"
|
|
||||||
import type { Snippet } from "@budibase/types"
|
|
||||||
|
|
||||||
export let addSnippet
|
|
||||||
export let snippets
|
|
||||||
export let search
|
|
||||||
|
|
||||||
let popover: Popover
|
|
||||||
let popoverAnchor: HTMLElement | null
|
|
||||||
let hoverTarget: {
|
|
||||||
type: "binding" | "helper" | "snippet"
|
|
||||||
code: string
|
|
||||||
description?: string
|
|
||||||
} | null
|
|
||||||
let hideTimeout: ReturnType<typeof setTimeout> | null
|
|
||||||
let snippetDrawer: SnippetDrawer
|
|
||||||
let editableSnippet: Snippet | null
|
|
||||||
|
|
||||||
$: enableSnippets = !$licensing.isFreePlan
|
|
||||||
|
|
||||||
$: filteredSnippets = getFilteredSnippets(enableSnippets, snippets, search)
|
|
||||||
|
|
||||||
const showSnippet = (snippet: Snippet, target: HTMLElement) => {
|
|
||||||
stopHidingPopover()
|
|
||||||
if (!snippet.code) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
popoverAnchor = target
|
|
||||||
hoverTarget = {
|
|
||||||
type: "snippet",
|
|
||||||
code: snippet.code,
|
|
||||||
}
|
|
||||||
popover.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const hidePopover = () => {
|
|
||||||
hideTimeout = setTimeout(() => {
|
|
||||||
popover.hide()
|
|
||||||
popoverAnchor = null
|
|
||||||
hoverTarget = null
|
|
||||||
hideTimeout = null
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopHidingPopover = () => {
|
|
||||||
if (hideTimeout) {
|
|
||||||
clearTimeout(hideTimeout)
|
|
||||||
hideTimeout = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createSnippet = () => {
|
|
||||||
editableSnippet = null
|
|
||||||
snippetDrawer.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const editSnippet = (e: Event, snippet: Snippet) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
editableSnippet = snippet
|
|
||||||
snippetDrawer.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<div class="snippet-list">
|
|
||||||
{#if enableSnippets}
|
|
||||||
{#each filteredSnippets as snippet}
|
|
||||||
<div
|
|
||||||
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)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/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 hoverTarget && popoverAnchor}
|
|
||||||
<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 hoverTarget}
|
|
||||||
<CodeEditor
|
|
||||||
value={hoverTarget.code?.trim()}
|
|
||||||
mode={EditorModes.JS}
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
{/key}
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<SnippetDrawer bind:this={snippetDrawer} snippet={editableSnippet} />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* 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>
|
|
|
@ -1,46 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Icon, Tags, Tag } from "@budibase/bbui"
|
|
||||||
import { licensing } from "@/stores/portal"
|
|
||||||
import SnippetDrawer from "./SnippetDrawer.svelte"
|
|
||||||
import type { Snippet } from "@budibase/types"
|
|
||||||
|
|
||||||
$: enableSnippets = !$licensing.isFreePlan
|
|
||||||
|
|
||||||
let snippetDrawer: SnippetDrawer
|
|
||||||
let editableSnippet: Snippet | null
|
|
||||||
|
|
||||||
const createSnippet = () => {
|
|
||||||
editableSnippet = null
|
|
||||||
snippetDrawer.show()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if enableSnippets}
|
|
||||||
<div class="add-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}
|
|
||||||
|
|
||||||
<SnippetDrawer bind:this={snippetDrawer} snippet={editableSnippet} />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.title {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
.add-button {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
Loading…
Reference in New Issue