Add back in working component tree with scrolling and dropdown menus
This commit is contained in:
parent
853ca15bb4
commit
168ec6634e
|
@ -2,7 +2,7 @@ import { getFrontendStore } from "./store/frontend"
|
||||||
import { getAutomationStore } from "./store/automation"
|
import { getAutomationStore } from "./store/automation"
|
||||||
import { getThemeStore } from "./store/theme"
|
import { getThemeStore } from "./store/theme"
|
||||||
import { derived, writable } from "svelte/store"
|
import { derived, writable } from "svelte/store"
|
||||||
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
import { LAYOUT_NAMES } from "../constants"
|
||||||
import { findComponent, findComponentPath } from "./componentUtils"
|
import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
|
|
||||||
export const store = getFrontendStore()
|
export const store = getFrontendStore()
|
||||||
|
@ -13,32 +13,26 @@ export const selectedScreen = derived(store, $store => {
|
||||||
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
|
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const currentAsset = selectedScreen
|
|
||||||
|
|
||||||
export const selectedComponent = derived(
|
export const selectedComponent = derived(
|
||||||
[store, currentAsset],
|
[store, selectedScreen],
|
||||||
([$store, $currentAsset]) => {
|
([$store, $selectedScreen]) => {
|
||||||
if (!$currentAsset || !$store.selectedComponentId) {
|
if (!$selectedScreen || !$store.selectedComponentId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return findComponent($currentAsset?.props, $store.selectedComponentId)
|
return findComponent($selectedScreen?.props, $store.selectedComponentId)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const selectedComponentPath = derived(
|
export const selectedComponentPath = derived(
|
||||||
[store, currentAsset],
|
[store, selectedScreen],
|
||||||
([$store, $currentAsset]) => {
|
([$store, $selectedScreen]) => {
|
||||||
return findComponentPath(
|
return findComponentPath(
|
||||||
$currentAsset?.props,
|
$selectedScreen?.props,
|
||||||
$store.selectedComponentId
|
$store.selectedComponentId
|
||||||
).map(component => component._id)
|
).map(component => component._id)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const currentAssetName = derived(currentAsset, $currentAsset => {
|
|
||||||
return $currentAsset?.name
|
|
||||||
})
|
|
||||||
|
|
||||||
export const mainLayout = derived(store, $store => {
|
export const mainLayout = derived(store, $store => {
|
||||||
return $store.layouts?.find(
|
return $store.layouts?.find(
|
||||||
layout => layout._id === LAYOUT_NAMES.MASTER.PRIVATE
|
layout => layout._id === LAYOUT_NAMES.MASTER.PRIVATE
|
||||||
|
@ -47,4 +41,5 @@ export const mainLayout = derived(store, $store => {
|
||||||
|
|
||||||
export const selectedAccessRole = writable("BASIC")
|
export const selectedAccessRole = writable("BASIC")
|
||||||
|
|
||||||
export const screenSearchString = writable(null)
|
// For compatibility
|
||||||
|
export const currentAsset = selectedScreen
|
||||||
|
|
|
@ -48,7 +48,6 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
},
|
},
|
||||||
currentFrontEndType: "none",
|
currentFrontEndType: "none",
|
||||||
selectedLayoutId: "",
|
selectedLayoutId: "",
|
||||||
selectedComponentId: "",
|
|
||||||
errors: [],
|
errors: [],
|
||||||
hasAppPackage: false,
|
hasAppPackage: false,
|
||||||
libraries: null,
|
libraries: null,
|
||||||
|
@ -61,6 +60,7 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
|
|
||||||
// URL params
|
// URL params
|
||||||
selectedScreenId: null,
|
selectedScreenId: null,
|
||||||
|
selectedComponentId: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFrontendStore = () => {
|
export const getFrontendStore = () => {
|
||||||
|
@ -300,32 +300,6 @@ export const getFrontendStore = () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
select: component => {
|
|
||||||
const asset = get(currentAsset)
|
|
||||||
if (!asset || !component) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is the root component, select the asset instead
|
|
||||||
const parent = findComponentParent(asset.props, component._id)
|
|
||||||
if (parent == null) {
|
|
||||||
const state = get(store)
|
|
||||||
const isLayout = state.currentFrontEndType === FrontendTypes.LAYOUT
|
|
||||||
if (isLayout) {
|
|
||||||
store.actions.layouts.select(asset._id)
|
|
||||||
} else {
|
|
||||||
store.actions.screens.select(asset._id)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise select the component
|
|
||||||
store.update(state => {
|
|
||||||
state.selectedComponentId = component._id
|
|
||||||
state.currentView = "component"
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getDefinition: componentName => {
|
getDefinition: componentName => {
|
||||||
if (!componentName) {
|
if (!componentName) {
|
||||||
return null
|
return null
|
||||||
|
@ -464,7 +438,10 @@ export const getFrontendStore = () => {
|
||||||
parent._children = parent._children.filter(
|
parent._children = parent._children.filter(
|
||||||
child => child._id !== component._id
|
child => child._id !== component._id
|
||||||
)
|
)
|
||||||
store.actions.components.select(parent)
|
store.update(state => {
|
||||||
|
state.selectedComponentId = parent._id
|
||||||
|
return state
|
||||||
|
})
|
||||||
}
|
}
|
||||||
await store.actions.preview.saveSelected()
|
await store.actions.preview.saveSelected()
|
||||||
},
|
},
|
||||||
|
@ -488,7 +465,10 @@ export const getFrontendStore = () => {
|
||||||
parent._children = parent._children.filter(
|
parent._children = parent._children.filter(
|
||||||
child => child._id !== component._id
|
child => child._id !== component._id
|
||||||
)
|
)
|
||||||
store.actions.components.select(parent)
|
store.update(state => {
|
||||||
|
state.selectedComponentId = parent._id
|
||||||
|
return state
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -539,7 +519,7 @@ export const getFrontendStore = () => {
|
||||||
|
|
||||||
// Save and select the new component
|
// Save and select the new component
|
||||||
promises.push(store.actions.preview.saveSelected())
|
promises.push(store.actions.preview.saveSelected())
|
||||||
store.actions.components.select(componentToPaste)
|
state.selectedComponentId = componentToPaste._id
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
ActionMenu,
|
|
||||||
ActionButton,
|
|
||||||
MenuItem,
|
|
||||||
Icon,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { store, currentAssetName, selectedComponent } from "builderStore"
|
|
||||||
import structure from "./componentStructure.json"
|
|
||||||
|
|
||||||
$: enrichedStructure = enrichStructure(structure, $store.components)
|
|
||||||
|
|
||||||
const isChildAllowed = ({ name }, selectedComponent) => {
|
|
||||||
const currentComponent = store.actions.components.getDefinition(
|
|
||||||
selectedComponent?._component
|
|
||||||
)
|
|
||||||
return currentComponent?.illegalChildren?.includes(name.toLowerCase())
|
|
||||||
}
|
|
||||||
|
|
||||||
const enrichStructure = (structure, definitions) => {
|
|
||||||
let enrichedStructure = []
|
|
||||||
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 onItemChosen = async item => {
|
|
||||||
if (!item.isCategory) {
|
|
||||||
try {
|
|
||||||
await store.actions.components.create(item.component)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error creating component")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="components">
|
|
||||||
{#each enrichedStructure as item}
|
|
||||||
<ActionMenu disabled={!item.isCategory}>
|
|
||||||
<ActionButton
|
|
||||||
icon={item.icon}
|
|
||||||
disabled={!item.isCategory && isChildAllowed(item, $selectedComponent)}
|
|
||||||
quiet
|
|
||||||
size="S"
|
|
||||||
slot="control"
|
|
||||||
dataCy={`category-${item.name}`}
|
|
||||||
on:click={() => onItemChosen(item)}
|
|
||||||
>
|
|
||||||
<div class="buttonContent">
|
|
||||||
{item.name}
|
|
||||||
{#if item.isCategory}
|
|
||||||
<Icon size="S" name="ChevronDown" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</ActionButton>
|
|
||||||
{#each item.children || [] as item}
|
|
||||||
{#if !item.showOnAsset || item.showOnAsset.includes($currentAssetName)}
|
|
||||||
<MenuItem
|
|
||||||
dataCy={`component-${item.name}`}
|
|
||||||
icon={item.icon}
|
|
||||||
on:click={() => onItemChosen(item)}
|
|
||||||
disabled={isChildAllowed(item, $selectedComponent)}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</MenuItem>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</ActionMenu>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.components {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.components :global(> *) {
|
|
||||||
height: 32px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonContent {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
.buttonContent :global(svg) {
|
|
||||||
margin-left: 2px !important;
|
|
||||||
margin-right: 0 !important;
|
|
||||||
margin-bottom: -1px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -127,9 +127,6 @@
|
||||||
<Tab title="Screens">
|
<Tab title="Screens">
|
||||||
<div class="tab-content-padding">
|
<div class="tab-content-padding">
|
||||||
<BBUILayout noPadding gap="XS" />
|
<BBUILayout noPadding gap="XS" />
|
||||||
<div class="nav-items-container" bind:this={scrollRef}>
|
|
||||||
<ComponentNavigationTree />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Layouts">
|
<Tab title="Layouts">
|
||||||
|
@ -190,15 +187,6 @@
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-items-container {
|
|
||||||
border-top: var(--border-light);
|
|
||||||
margin: 0 calc(-1 * var(--spacing-xl));
|
|
||||||
padding: var(--spacing-m) 0;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: auto;
|
|
||||||
height: 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.nav-items-container--layouts {
|
.nav-items-container--layouts {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
margin-top: calc(-1 * var(--spectrum-global-dimension-static-size-150));
|
margin-top: calc(-1 * var(--spectrum-global-dimension-static-size-150));
|
||||||
|
|
|
@ -23,7 +23,9 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<div class="body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -35,7 +37,6 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
overflow: auto;
|
|
||||||
}
|
}
|
||||||
.navigation-panel.wide {
|
.navigation-panel.wide {
|
||||||
width: 360px;
|
width: 360px;
|
||||||
|
@ -78,4 +79,13 @@
|
||||||
.add-button :global(svg) {
|
.add-button :global(svg) {
|
||||||
fill: white;
|
fill: white;
|
||||||
}
|
}
|
||||||
|
.body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -64,20 +64,11 @@
|
||||||
{#each paths as path, idx (path)}
|
{#each paths as path, idx (path)}
|
||||||
<PathTree border={idx > 0} {path} route={routes[path]} />
|
<PathTree border={idx > 0} {path} route={routes[path]} />
|
||||||
{/each}
|
{/each}
|
||||||
{#if !paths.length}
|
{#if !paths.length}{/if}
|
||||||
<div class="empty">
|
|
||||||
There aren't any screens configured with this access role.
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.root.has-screens {
|
.root.has-screens {
|
||||||
min-width: max-content;
|
min-width: max-content;
|
||||||
}
|
}
|
||||||
div.empty {
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
color: var(--grey-5);
|
|
||||||
padding: var(--spacing-xs) var(--spacing-xl);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
|
@ -1,10 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { store, selectedComponent, currentAsset } from "builderStore"
|
||||||
store,
|
|
||||||
selectedComponent,
|
|
||||||
currentAsset,
|
|
||||||
screenSearchString,
|
|
||||||
} from "builderStore"
|
|
||||||
import instantiateStore from "./dragDropStore"
|
import instantiateStore from "./dragDropStore"
|
||||||
import ComponentTree from "./ComponentTree.svelte"
|
import ComponentTree from "./ComponentTree.svelte"
|
||||||
import PathDropdownMenu from "./PathDropdownMenu.svelte"
|
import PathDropdownMenu from "./PathDropdownMenu.svelte"
|
|
@ -12,7 +12,7 @@
|
||||||
$: roleName = $roles.find(x => x._id === roleId)?.name || "Unknown"
|
$: roleName = $roles.find(x => x._id === roleId)?.name || "Unknown"
|
||||||
|
|
||||||
// Needs to be absolute as we embed this component from multiple different URLs
|
// Needs to be absolute as we embed this component from multiple different URLs
|
||||||
$: newComponentUrl = `/builder/app/${store.appId}/design/components/${$selectedScreen?._id}/new`
|
$: newComponentUrl = `/builder/app/${$store.appId}/design/components/${$selectedScreen?._id}/new`
|
||||||
|
|
||||||
const getRoleColor = roleId => {
|
const getRoleColor = roleId => {
|
||||||
return RoleColours[roleId] || "#ffa500"
|
return RoleColours[roleId] || "#ffa500"
|
||||||
|
|
|
@ -140,7 +140,10 @@
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (type === "select-component" && data.id) {
|
if (type === "select-component" && data.id) {
|
||||||
store.actions.components.select({ _id: data.id })
|
store.update(state => {
|
||||||
|
state.selectedComponentId = data.id
|
||||||
|
return state
|
||||||
|
})
|
||||||
} else if (type === "update-prop") {
|
} else if (type === "update-prop") {
|
||||||
await store.actions.components.updateProp(data.prop, data.value)
|
await store.actions.components.updateProp(data.prop, data.value)
|
||||||
} else if (type === "delete-component" && data.id) {
|
} else if (type === "delete-component" && data.id) {
|
||||||
|
|
|
@ -1,9 +1,63 @@
|
||||||
<script>
|
<script>
|
||||||
import NavigationPanel from "components/design/NavigationPanel/NavigationPanel.svelte"
|
import NavigationPanel from "components/design/NavigationPanel/NavigationPanel.svelte"
|
||||||
|
import ComponentTree from "./ComponentTree.svelte"
|
||||||
|
import instantiateStore from "./dragDropStore.js"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { Layout, Search } from "@budibase/bbui"
|
import { store, selectedScreen, selectedComponent } from "builderStore"
|
||||||
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
|
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
||||||
|
import { setContext } from "svelte"
|
||||||
|
|
||||||
let searchString
|
const dragDropStore = instantiateStore()
|
||||||
|
let scrollRef
|
||||||
|
|
||||||
|
const scrollTo = bounds => {
|
||||||
|
if (!bounds) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sidebarWidth = 259
|
||||||
|
const navItemHeight = 32
|
||||||
|
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
|
||||||
|
let scrollBounds = scrollRef.getBoundingClientRect()
|
||||||
|
let newOffsets = {}
|
||||||
|
|
||||||
|
// Calculate left offset
|
||||||
|
const offsetX = bounds.left + bounds.width + scrollLeft - 40
|
||||||
|
if (offsetX > sidebarWidth) {
|
||||||
|
newOffsets.left = offsetX - sidebarWidth
|
||||||
|
} else {
|
||||||
|
newOffsets.left = 0
|
||||||
|
}
|
||||||
|
if (newOffsets.left === scrollLeft) {
|
||||||
|
delete newOffsets.left
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate top offset
|
||||||
|
const offsetY = bounds.top - scrollBounds?.top + scrollTop
|
||||||
|
if (offsetY > scrollTop + offsetHeight - 2 * navItemHeight) {
|
||||||
|
newOffsets.top = offsetY - offsetHeight + 2 * navItemHeight
|
||||||
|
} else if (offsetY < scrollTop + navItemHeight) {
|
||||||
|
newOffsets.top = offsetY - navItemHeight
|
||||||
|
} else {
|
||||||
|
delete newOffsets.top
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if offset is unchanged
|
||||||
|
if (newOffsets.left == null && newOffsets.top == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smoothly scroll to the offset
|
||||||
|
scrollRef.scroll({
|
||||||
|
...newOffsets,
|
||||||
|
behavior: "smooth",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set scroll context so components can invoke scrolling when selected
|
||||||
|
setContext("scroll", {
|
||||||
|
scrollTo,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<NavigationPanel
|
<NavigationPanel
|
||||||
|
@ -11,11 +65,36 @@
|
||||||
showAddButton
|
showAddButton
|
||||||
onClickAddButton={() => $goto("../new")}
|
onClickAddButton={() => $goto("../new")}
|
||||||
>
|
>
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
<div class="nav-items-container" bind:this={scrollRef}>
|
||||||
<Search
|
<NavItem
|
||||||
placeholder="Search"
|
text="Screen"
|
||||||
value={searchString}
|
withArrow
|
||||||
on:change={e => (searchString = e.detail)}
|
indentLevel={0}
|
||||||
|
selected={$store.selectedComponentId === $selectedScreen?.props._id}
|
||||||
|
opened
|
||||||
|
scrollable
|
||||||
|
on:click={() => {
|
||||||
|
$store.selectedComponentId = $selectedScreen?.props._id
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
||||||
|
</NavItem>
|
||||||
|
<ComponentTree
|
||||||
|
level={0}
|
||||||
|
components={$selectedScreen?.props._children}
|
||||||
|
currentComponent={$selectedComponent}
|
||||||
|
{dragDropStore}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</div>
|
||||||
</NavigationPanel>
|
</NavigationPanel>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-items-container {
|
||||||
|
margin: 0 calc(-1 * var(--spacing-l));
|
||||||
|
padding: var(--spacing-xl) var(--spacing-l);
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -9,16 +9,11 @@
|
||||||
|
|
||||||
export let components = []
|
export let components = []
|
||||||
export let currentComponent
|
export let currentComponent
|
||||||
export let onSelect = () => {}
|
|
||||||
export let level = 0
|
export let level = 0
|
||||||
export let dragDropStore
|
export let dragDropStore
|
||||||
|
|
||||||
let closedNodes = {}
|
let closedNodes = {}
|
||||||
|
|
||||||
const selectComponent = component => {
|
|
||||||
store.actions.components.select(component)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dragstart = component => e => {
|
const dragstart = component => e => {
|
||||||
e.dataTransfer.dropEffect = DropEffect.MOVE
|
e.dataTransfer.dropEffect = DropEffect.MOVE
|
||||||
dragDropStore.actions.dragstart(component)
|
dragDropStore.actions.dragstart(component)
|
||||||
|
@ -86,7 +81,11 @@
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{#each components || [] as component, index (component._id)}
|
{#each components || [] as component, index (component._id)}
|
||||||
<li on:click|stopPropagation={() => selectComponent(component)}>
|
<li
|
||||||
|
on:click|stopPropagation={() => {
|
||||||
|
$store.selectedComponentId = component._id
|
||||||
|
}}
|
||||||
|
>
|
||||||
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
|
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
|
||||||
<div
|
<div
|
||||||
on:drop={onDrop}
|
on:drop={onDrop}
|
||||||
|
@ -98,6 +97,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<NavItem
|
<NavItem
|
||||||
|
scrollable
|
||||||
draggable
|
draggable
|
||||||
on:dragend={dragDropStore.actions.reset}
|
on:dragend={dragDropStore.actions.reset}
|
||||||
on:dragstart={dragstart(component)}
|
on:dragstart={dragstart(component)}
|
||||||
|
@ -117,7 +117,6 @@
|
||||||
<svelte:self
|
<svelte:self
|
||||||
components={component._children}
|
components={component._children}
|
||||||
{currentComponent}
|
{currentComponent}
|
||||||
{onSelect}
|
|
||||||
{dragDropStore}
|
{dragDropStore}
|
||||||
level={level + 1}
|
level={level + 1}
|
||||||
/>
|
/>
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script>
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let component
|
||||||
|
|
||||||
|
$: definition = store.actions.components.getDefinition(component?._component)
|
||||||
|
$: noPaste = !$store.componentToPaste
|
||||||
|
|
||||||
|
// "editable" has been repurposed for inline text editing.
|
||||||
|
// It remains here for legacy compatibility.
|
||||||
|
// Future components should define "static": true for indicate they should
|
||||||
|
// not show a context menu.
|
||||||
|
$: showMenu = definition?.editable !== false && definition?.static !== true
|
||||||
|
|
||||||
|
const storeComponentForCopy = (cut = false) => {
|
||||||
|
store.actions.components.copy(component, cut)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pasteComponent = mode => {
|
||||||
|
try {
|
||||||
|
store.actions.components.paste(component, mode)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error saving component")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if showMenu}
|
||||||
|
<ActionMenu>
|
||||||
|
<div slot="control" class="icon">
|
||||||
|
<Icon size="S" hoverable name="MoreSmallList" />
|
||||||
|
</div>
|
||||||
|
<MenuItem icon="Copy" on:click={() => storeComponentForCopy(false)}>
|
||||||
|
Copy
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon="ShowOneLayer"
|
||||||
|
on:click={() => pasteComponent("inside")}
|
||||||
|
disabled={noPaste}
|
||||||
|
>
|
||||||
|
Paste inside
|
||||||
|
</MenuItem>
|
||||||
|
</ActionMenu>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.icon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script>
|
||||||
|
import { syncURLToState } from "helpers/urlStateSync"
|
||||||
|
import { store, selectedScreen } from "builderStore"
|
||||||
|
import { goto, params, redirect } from "@roxi/routify"
|
||||||
|
import { onDestroy } from "svelte"
|
||||||
|
import { findComponent } from "builderStore/componentUtils"
|
||||||
|
|
||||||
|
// Keep URL and state in sync for selected component ID
|
||||||
|
const stopSyncing = syncURLToState({
|
||||||
|
keys: [
|
||||||
|
{
|
||||||
|
url: "componentId",
|
||||||
|
state: "selectedComponentId",
|
||||||
|
validate: componentId => {
|
||||||
|
return !!findComponent($selectedScreen.props, componentId)
|
||||||
|
},
|
||||||
|
fallbackUrl: "../",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
store,
|
||||||
|
params,
|
||||||
|
goto,
|
||||||
|
redirect,
|
||||||
|
baseUrl: "..",
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(stopSyncing)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
|
@ -18,6 +18,7 @@
|
||||||
params,
|
params,
|
||||||
goto,
|
goto,
|
||||||
redirect,
|
redirect,
|
||||||
|
baseUrl: "..",
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(stopSyncing)
|
onDestroy(stopSyncing)
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($selectedScreen) {
|
if ($selectedScreen) {
|
||||||
// Select the screen slot if a screen exists
|
// Select the screen slot if a screen exists
|
||||||
$redirect(`./slot`)
|
$redirect(`./${$selectedScreen.props._id}`)
|
||||||
} else {
|
} else {
|
||||||
// Otherwise go up so we can select a new valid screen
|
// Otherwise go up so we can select a new valid screen
|
||||||
$redirect("../")
|
$redirect("../")
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
Icon,
|
Icon,
|
||||||
Body,
|
Body,
|
||||||
Divider,
|
Divider,
|
||||||
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import structure from "./componentStructure.json"
|
import structure from "./componentStructure.json"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
@ -75,7 +76,20 @@
|
||||||
return structure
|
return structure
|
||||||
}
|
}
|
||||||
|
|
||||||
const addComponent = () => {}
|
const isChildAllowed = ({ name }, selectedComponent) => {
|
||||||
|
const currentComponent = store.actions.components.getDefinition(
|
||||||
|
selectedComponent?._component
|
||||||
|
)
|
||||||
|
return currentComponent?.illegalChildren?.includes(name.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
const addComponent = async item => {
|
||||||
|
try {
|
||||||
|
await store.actions.components.create(item.component)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error creating component")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<NavigationPanel
|
<NavigationPanel
|
||||||
|
@ -125,7 +139,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||||
<Body size="S">Blocks are a collection of pre-built components</Body>
|
<Body>Blocks are a collection of pre-built components</Body>
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
{#each blocks as block}
|
{#each blocks as block}
|
||||||
<div class="component block">
|
<div class="component block">
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script>
|
||||||
|
import SettingsPanel from "components/design/SettingsPanel/SettingsPanel.svelte"
|
||||||
|
import { Body, Layout } from "@budibase/bbui"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SettingsPanel title="Screen" icon="WebPage">
|
||||||
|
<Layout paddingX="L" paddingY="XL">
|
||||||
|
<Body>The component you add will be placed inside Screen</Body>
|
||||||
|
</Layout>
|
||||||
|
</SettingsPanel>
|
|
@ -1,5 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import ComponentListPanel from "./_components/ComponentListPanel.svelte"
|
import NewComponentPanel from "./_components/NewComponentPanel.svelte"
|
||||||
|
import NewComponentTargetPanel from "./_components/NewComponentTargetPanel.svelte"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ComponentListPanel />
|
<NewComponentPanel />
|
||||||
|
<NewComponentTargetPanel />
|
||||||
|
|
|
@ -67,6 +67,19 @@
|
||||||
<ScreenDropdownMenu screenId={screen._id} />
|
<ScreenDropdownMenu screenId={screen._id} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if !filteredScreens?.length}
|
||||||
|
<div class="empty">
|
||||||
|
There aren't any screens matching the current filters
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</NavigationPanel>
|
</NavigationPanel>
|
||||||
|
|
||||||
<ScreenWizard bind:showModal={showNewScreenModal} />
|
<ScreenWizard bind:showModal={showNewScreenModal} />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.empty {
|
||||||
|
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||||
|
color: var(--grey-7);
|
||||||
|
padding: 0 var(--spacing-l);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -6,9 +6,7 @@
|
||||||
|
|
||||||
<NavigationPanel title="Theme">
|
<NavigationPanel title="Theme">
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||||
<Body size="S">
|
<Body>Your theme is set across all the screens within your app</Body>
|
||||||
Your theme is set across all the screens within your app
|
|
||||||
</Body>
|
|
||||||
<ThemeEditor />
|
<ThemeEditor />
|
||||||
</Layout>
|
</Layout>
|
||||||
</NavigationPanel>
|
</NavigationPanel>
|
||||||
|
|
Loading…
Reference in New Issue