Merge pull request #4987 from Budibase/scrollable-component-tree
Scrollable component tree
This commit is contained in:
commit
38be4979f3
|
@ -3,7 +3,7 @@ import { getAutomationStore } from "./store/automation"
|
|||
import { getThemeStore } from "./store/theme"
|
||||
import { derived, writable } from "svelte/store"
|
||||
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
||||
import { findComponent } from "./componentUtils"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
|
||||
export const store = getFrontendStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
|
@ -29,6 +29,16 @@ export const selectedComponent = derived(
|
|||
}
|
||||
)
|
||||
|
||||
export const selectedComponentPath = derived(
|
||||
[store, currentAsset],
|
||||
([$store, $currentAsset]) => {
|
||||
return findComponentPath(
|
||||
$currentAsset?.props,
|
||||
$store.selectedComponentId
|
||||
).map(component => component._id)
|
||||
}
|
||||
)
|
||||
|
||||
export const currentAssetId = derived(store, $store => {
|
||||
return $store.currentFrontEndType === FrontendTypes.SCREEN
|
||||
? $store.selectedScreenId
|
||||
|
|
|
@ -10,12 +10,10 @@
|
|||
<div class="title">
|
||||
<Tabs selected="Automations">
|
||||
<Tab title="Automations">
|
||||
<div class="tab-content-padding">
|
||||
<AutomationList />
|
||||
<Modal bind:this={modal}>
|
||||
<CreateAutomationModal {webhookModal} />
|
||||
</Modal>
|
||||
</div>
|
||||
<AutomationList />
|
||||
<Modal bind:this={modal}>
|
||||
<CreateAutomationModal {webhookModal} />
|
||||
</Modal>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<div class="add-button" data-cy="new-screen">
|
||||
|
@ -24,9 +22,6 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
}
|
||||
.add-button {
|
||||
position: absolute;
|
||||
top: var(--spacing-l);
|
||||
|
|
|
@ -22,10 +22,11 @@
|
|||
const selected = $datasources.selected === datasource._id
|
||||
const open = openDataSources.includes(datasource._id)
|
||||
const containsSelected = containsActiveEntity(datasource)
|
||||
const onlySource = $datasources.list.length === 1
|
||||
return {
|
||||
...datasource,
|
||||
selected,
|
||||
open: selected || open || containsSelected,
|
||||
open: selected || open || containsSelected || onlySource,
|
||||
}
|
||||
})
|
||||
: []
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { createEventDispatcher, getContext } from "svelte"
|
||||
|
||||
export let icon
|
||||
export let withArrow = false
|
||||
|
@ -14,29 +14,46 @@
|
|||
export let iconText
|
||||
export let iconColor
|
||||
|
||||
const scrollApi = getContext("scroll")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function onIconClick(event) {
|
||||
event.stopPropagation()
|
||||
let contentRef
|
||||
$: selected && contentRef && scrollToView()
|
||||
|
||||
const onClick = () => {
|
||||
scrollToView()
|
||||
dispatch("click")
|
||||
}
|
||||
|
||||
const onIconClick = e => {
|
||||
e.stopPropagation()
|
||||
dispatch("iconClick")
|
||||
}
|
||||
|
||||
const scrollToView = () => {
|
||||
if (!scrollApi || !contentRef) {
|
||||
return
|
||||
}
|
||||
const bounds = contentRef.getBoundingClientRect()
|
||||
scrollApi.scrollTo(bounds)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="nav-item"
|
||||
class:border
|
||||
class:selected
|
||||
style={`padding-left: ${indentLevel * 14}px`}
|
||||
style={`padding-left: ${20 + indentLevel * 14}px`}
|
||||
{draggable}
|
||||
on:dragend
|
||||
on:dragstart
|
||||
on:dragover
|
||||
on:drop
|
||||
on:click
|
||||
on:click={onClick}
|
||||
ondragover="return false"
|
||||
ondragenter="return false"
|
||||
>
|
||||
<div class="content">
|
||||
<div class="nav-item-content" bind:this={contentRef}>
|
||||
{#if withArrow}
|
||||
<div class:opened class="icon arrow" on:click={onIconClick}>
|
||||
<Icon size="S" name="ChevronRight" />
|
||||
|
@ -64,11 +81,16 @@
|
|||
|
||||
<style>
|
||||
.nav-item {
|
||||
border-radius: var(--border-radius-s);
|
||||
cursor: pointer;
|
||||
color: var(--grey-7);
|
||||
transition: background-color
|
||||
var(--spectrum-global-animation-duration-100, 130ms) ease-in-out;
|
||||
padding: 0 var(--spacing-m) 0 var(--spacing-xl);
|
||||
height: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.nav-item.selected {
|
||||
background-color: var(--grey-2);
|
||||
|
@ -81,14 +103,14 @@
|
|||
visibility: visible;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 var(--spacing-s);
|
||||
height: 32px;
|
||||
.nav-item-content {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
@ -111,12 +133,13 @@
|
|||
}
|
||||
|
||||
.text {
|
||||
flex: 1 1 auto;
|
||||
font-weight: 600;
|
||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
@ -125,9 +148,9 @@
|
|||
height: 20px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
margin-left: var(--spacing-s);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.iconText {
|
||||
|
|
|
@ -138,3 +138,10 @@
|
|||
onOk={deleteComponent}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import NavItem from "components/common/NavItem.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { selectedComponentPath } from "builderStore"
|
||||
|
||||
export let components = []
|
||||
export let currentComponent
|
||||
|
@ -71,10 +72,20 @@
|
|||
notifications.error("Error saving component")
|
||||
}
|
||||
}
|
||||
|
||||
const isOpen = (component, selectedComponentPath, closedNodes) => {
|
||||
if (!component?._children?.length) {
|
||||
return false
|
||||
}
|
||||
if (selectedComponentPath.includes(component._id)) {
|
||||
return true
|
||||
}
|
||||
return !closedNodes[component._id]
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#each components as component, index (component._id)}
|
||||
{#each components || [] as component, index (component._id)}
|
||||
<li on:click|stopPropagation={() => selectComponent(component)}>
|
||||
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
|
||||
<div
|
||||
|
@ -97,12 +108,12 @@
|
|||
withArrow
|
||||
indentLevel={level + 1}
|
||||
selected={$store.selectedComponentId === component._id}
|
||||
opened={!closedNodes[component._id] && component?._children?.length}
|
||||
opened={isOpen(component, $selectedComponentPath, closedNodes)}
|
||||
>
|
||||
<ComponentDropdownMenu {component} />
|
||||
</NavItem>
|
||||
|
||||
{#if component._children && !closedNodes[component._id]}
|
||||
{#if isOpen(component, $selectedComponentPath, closedNodes)}
|
||||
<svelte:self
|
||||
components={component._children}
|
||||
{currentComponent}
|
||||
|
@ -133,6 +144,10 @@
|
|||
padding-left: 0;
|
||||
margin: 0;
|
||||
}
|
||||
ul,
|
||||
li {
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.drop-item {
|
||||
border-radius: var(--border-radius-m);
|
||||
|
|
|
@ -65,3 +65,10 @@
|
|||
<Input thin type="text" label="Name" bind:value={name} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -75,4 +75,8 @@
|
|||
align-items: flex-start;
|
||||
padding-left: var(--spacing-xl);
|
||||
}
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
export let border
|
||||
|
||||
let routeManuallyOpened = false
|
||||
|
||||
$: selectedScreen = $currentAsset
|
||||
$: allScreens = getAllScreens(route)
|
||||
$: filteredScreens = getFilteredScreens(allScreens, $screenSearchString)
|
||||
|
@ -83,7 +84,8 @@
|
|||
<NavItem
|
||||
icon="WebPage"
|
||||
indentLevel={indent || 1}
|
||||
selected={$store.selectedScreenId === screen.id}
|
||||
selected={$store.selectedScreenId === screen.id &&
|
||||
$store.currentView === "detail"}
|
||||
opened={$store.selectedScreenId === screen.id}
|
||||
text={ROUTE_NAME_MAP[screen.route]?.[screen.role] || screen.route}
|
||||
withArrow={route.subpaths}
|
||||
|
|
|
@ -103,3 +103,10 @@
|
|||
confirmText="Duplicate"
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -55,11 +55,10 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="root" class:has-screens={!!paths?.length}>
|
||||
{#each paths as path, idx (path)}
|
||||
<PathTree border={idx > 0} {path} route={routes[path]} />
|
||||
{/each}
|
||||
|
||||
{#if !paths.length}
|
||||
<div class="empty">
|
||||
There aren't any screens configured with this access role.
|
||||
|
@ -68,9 +67,12 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.root.has-screens {
|
||||
min-width: max-content;
|
||||
}
|
||||
div.empty {
|
||||
font-size: var(--font-size-xs);
|
||||
font-size: var(--font-size-s);
|
||||
color: var(--grey-5);
|
||||
padding-top: var(--spacing-xs);
|
||||
padding: var(--spacing-xs) var(--spacing-xl);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { onMount, setContext } from "svelte"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import {
|
||||
store,
|
||||
|
@ -18,11 +18,63 @@
|
|||
Search,
|
||||
Tabs,
|
||||
Tab,
|
||||
Layout as BBUILayout,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
|
||||
export let showModal
|
||||
|
||||
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 + 20
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
||||
setContext("scroll", {
|
||||
scrollTo,
|
||||
})
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
title: "Screens",
|
||||
|
@ -79,7 +131,7 @@
|
|||
<Tabs {selected} on:select={navigate}>
|
||||
<Tab title="Screens">
|
||||
<div class="tab-content-padding">
|
||||
<div class="role-select">
|
||||
<BBUILayout noPadding gap="XS">
|
||||
<Select
|
||||
on:change={updateAccessRole}
|
||||
value={$selectedAccessRole}
|
||||
|
@ -93,17 +145,24 @@
|
|||
label="Search Screens"
|
||||
bind:value={$screenSearchString}
|
||||
/>
|
||||
</div>
|
||||
<div class="nav-items-container">
|
||||
</BBUILayout>
|
||||
<div class="nav-items-container" bind:this={scrollRef}>
|
||||
<ComponentNavigationTree />
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab title="Layouts">
|
||||
<div class="tab-content-padding">
|
||||
{#each $store.layouts as layout, idx (layout._id)}
|
||||
<Layout {layout} border={idx > 0} />
|
||||
{/each}
|
||||
<div
|
||||
class="nav-items-container nav-items-container--layouts"
|
||||
bind:this={scrollRef}
|
||||
>
|
||||
<div class="layouts-container">
|
||||
{#each $store.layouts as layout, idx (layout._id)}
|
||||
<Layout {layout} border={idx > 0} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<Modal bind:this={newLayoutModal}>
|
||||
<NewLayoutModal />
|
||||
</Modal>
|
||||
|
@ -126,23 +185,45 @@
|
|||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.title :global(.spectrum-Tabs-content),
|
||||
.title :global(.spectrum-Tabs-content > div),
|
||||
.title :global(.spectrum-Tabs-content > div > div) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
position: absolute;
|
||||
top: var(--spacing-l);
|
||||
right: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.role-select {
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
margin-bottom: var(--spacing-m);
|
||||
gap: var(--spacing-m);
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.tab-content-padding {
|
||||
padding: 0 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 {
|
||||
border-top: none;
|
||||
margin-top: calc(-1 * var(--spectrum-global-dimension-static-size-150));
|
||||
}
|
||||
|
||||
.layouts-container {
|
||||
min-width: max-content;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,10 +23,8 @@
|
|||
<div class="nav">
|
||||
<Tabs {selected} on:select={selectFirstDatasource}>
|
||||
<Tab title="Sources">
|
||||
<div class="tab-content-padding">
|
||||
<DatasourceNavigator />
|
||||
<CreateDatasourceModal bind:modal />
|
||||
</div>
|
||||
<DatasourceNavigator />
|
||||
<CreateDatasourceModal bind:modal />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<div
|
||||
|
@ -63,10 +61,6 @@
|
|||
display: contents;
|
||||
}
|
||||
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
}
|
||||
|
||||
.nav {
|
||||
overflow-y: auto;
|
||||
background: var(--background);
|
||||
|
|
|
@ -244,8 +244,6 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-l);
|
||||
padding: 0 0 60px 0;
|
||||
overflow-y: auto;
|
||||
border-right: var(--border-light);
|
||||
}
|
||||
|
||||
|
|
|
@ -130,6 +130,7 @@
|
|||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
|
|
Loading…
Reference in New Issue