1
0
Fork 0
mirror of synced 2024-06-02 02:25:17 +12:00
budibase/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/NewComponentPanel.svelte
2022-09-07 13:37:45 +01:00

288 lines
7.9 KiB
Svelte

<script>
import Panel from "components/design/Panel.svelte"
import { goto } from "@roxi/routify"
import {
Layout,
ActionGroup,
ActionButton,
Search,
Icon,
Body,
notifications,
} from "@budibase/bbui"
import structure from "./componentStructure.json"
import { store, selectedComponent } from "builderStore"
import { onMount } from "svelte"
import { fly } from "svelte/transition"
let section = "components"
let searchString
let searchRef
let selectedIndex
let componentList = []
$: currentDefinition = store.actions.components.getDefinition(
$selectedComponent?._component
)
$: enrichedStructure = enrichStructure(
structure,
$store.components,
$store.customComponents
)
$: filteredStructure = filterStructure(
enrichedStructure,
section,
currentDefinition,
searchString
)
$: blocks = enrichedStructure.find(x => x.name === "Blocks").children
$: orderMap = createComponentOrderMap(componentList)
// Creates a simple lookup map from an array, so we can find the selected
// component much faster
const createComponentOrderMap = list => {
let map = {}
list.forEach((component, idx) => {
map[component] = idx
})
return map
}
// Parses the structure in the manifest and returns an enriched structure with
// explicit categories
const enrichStructure = (structure, definitions, customComponents) => {
let enrichedStructure = []
// Add custom components category
if (customComponents?.length) {
enrichedStructure.push({
name: "Plugins",
isCategory: true,
children: customComponents
.map(x => ({
...definitions[x],
name: definitions[x].friendlyName || definitions[x].name,
}))
.sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1
}),
})
}
structure.forEach(item => {
if (typeof item === "string") {
const def = definitions[`@budibase/standard-components/${item}`]
if (def) {
enrichedStructure.push({
...def,
isCategory: false,
})
}
} else {
enrichedStructure.push({
...item,
isCategory: true,
children: enrichStructure(item.children || [], definitions),
})
}
})
return enrichedStructure
}
const filterStructure = (structure, section, currentDefinition, search) => {
selectedIndex = search ? 0 : null
componentList = []
if (!structure?.length) {
return []
}
// Remove blocks if there is no search string
if (!search) {
structure = structure.filter(category => category.name !== "Blocks")
}
// Return only items which match the search string
let filteredStructure = []
structure.forEach(category => {
let matchedChildren = category.children.filter(child => {
const name = child.name.toLowerCase()
// Check if the component matches the search string
if (search && !name.includes(search.toLowerCase())) {
return false
}
// Check if the component is allowed as a child
return !currentDefinition?.illegalChildren?.includes(name)
})
if (matchedChildren.length) {
filteredStructure.push({
...category,
children: matchedChildren,
})
// Create a flat list of all components so that we can reference them by
// order later
componentList = componentList.concat(
matchedChildren.map(x => x.component)
)
}
})
structure = filteredStructure
return structure
}
const addComponent = async component => {
try {
await store.actions.components.create(component)
$goto("../")
} catch (error) {
notifications.error("Error creating component")
}
}
const handleKeyDown = e => {
if (e.key === "Tab") {
// Cycle selected components on tab press
if (selectedIndex == null) {
selectedIndex = 0
} else {
selectedIndex = (selectedIndex + 1) % componentList.length
}
e.preventDefault()
e.stopPropagation()
return false
} else if (e.key === "Enter") {
// Add selected component on enter press
if (componentList[selectedIndex]) {
addComponent(componentList[selectedIndex])
}
}
}
onMount(() => {
searchRef.focus()
window.addEventListener("keydown", handleKeyDown)
return () => {
window.removeEventListener("keydown", handleKeyDown)
}
})
</script>
<div class="container" transition:fly|local={{ x: 260, duration: 300 }}>
<Panel
title="Add component"
showCloseButton
onClickCloseButton={() => $goto("../")}
borderLeft
>
<Layout paddingX="L" paddingY="XL" gap="S">
<Search
placeholder="Search"
value={searchString}
on:change={e => (searchString = e.detail)}
bind:inputRef={searchRef}
/>
{#if !searchString}
<ActionGroup compact justified>
<ActionButton
fullWidth
selected={section === "components"}
on:click={() => (section = "components")}>Components</ActionButton
>
<ActionButton
fullWidth
selected={section === "blocks"}
on:click={() => (section = "blocks")}>Blocks</ActionButton
>
</ActionGroup>
{/if}
{#if searchString || section === "components"}
{#if filteredStructure.length}
{#each filteredStructure as category}
<Layout noPadding gap="XS">
<div class="category-label">{category.name}</div>
{#each category.children as component}
<div
data-cy={`component-${component.name}`}
class="component"
class:selected={selectedIndex ===
orderMap[component.component]}
on:click={() => addComponent(component.component)}
on:mouseover={() => (selectedIndex = null)}
>
<Icon name={component.icon} />
<Body size="XS">{component.name}</Body>
</div>
{/each}
</Layout>
{/each}
{:else}
<Body size="S">
There aren't any components matching the current filter
</Body>
{/if}
{:else}
<Body size="S">Blocks are collections of pre-built components</Body>
<Layout noPadding gap="XS">
{#each blocks as block}
<div
class="component"
on:click={() => addComponent(block.component)}
>
<Icon name={block.icon} />
<Body size="XS">{block.name}</Body>
</div>
{/each}
</Layout>
{/if}
</Layout>
</Panel>
</div>
<style>
.container {
position: fixed;
right: 0;
z-index: 1;
height: calc(100% - 60px);
display: flex;
flex-direction: row;
align-items: stretch;
}
.category-label {
color: var(--spectrum-global-color-gray-600);
text-transform: uppercase;
font-size: 12px;
font-weight: 600;
margin-top: var(--spacing-xs);
}
.component {
background: var(--spectrum-global-color-gray-200);
border-radius: 4px;
display: flex;
align-items: center;
border: 1px solid var(--spectrum-global-color-gray-200);
transition: background 130ms ease-out, border-color 130ms ease-out;
flex-direction: row;
justify-content: flex-start;
padding: var(--spacing-s) var(--spacing-l);
gap: var(--spacing-m);
overflow: hidden;
}
.component.selected {
border-color: var(--spectrum-global-color-blue-400);
}
.component:hover {
background: var(--spectrum-global-color-gray-300);
cursor: pointer;
}
.component :global(.spectrum-Body) {
line-height: 1.2 !important;
overflow: hidden;
text-overflow: ellipsis;
}
</style>