Merge pull request #4987 from Budibase/scrollable-component-tree

Scrollable component tree
This commit is contained in:
Andrew Kingston 2022-03-23 10:46:55 +00:00 committed by GitHub
commit 38be4979f3
15 changed files with 203 additions and 56 deletions

View File

@ -3,7 +3,7 @@ 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 { FrontendTypes, LAYOUT_NAMES } from "../constants"
import { findComponent } from "./componentUtils" import { findComponent, findComponentPath } from "./componentUtils"
export const store = getFrontendStore() export const store = getFrontendStore()
export const automationStore = getAutomationStore() 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 => { export const currentAssetId = derived(store, $store => {
return $store.currentFrontEndType === FrontendTypes.SCREEN return $store.currentFrontEndType === FrontendTypes.SCREEN
? $store.selectedScreenId ? $store.selectedScreenId

View File

@ -10,12 +10,10 @@
<div class="title"> <div class="title">
<Tabs selected="Automations"> <Tabs selected="Automations">
<Tab title="Automations"> <Tab title="Automations">
<div class="tab-content-padding"> <AutomationList />
<AutomationList /> <Modal bind:this={modal}>
<Modal bind:this={modal}> <CreateAutomationModal {webhookModal} />
<CreateAutomationModal {webhookModal} /> </Modal>
</Modal>
</div>
</Tab> </Tab>
</Tabs> </Tabs>
<div class="add-button" data-cy="new-screen"> <div class="add-button" data-cy="new-screen">
@ -24,9 +22,6 @@
</div> </div>
<style> <style>
.tab-content-padding {
padding: 0 var(--spacing-xl);
}
.add-button { .add-button {
position: absolute; position: absolute;
top: var(--spacing-l); top: var(--spacing-l);

View File

@ -22,10 +22,11 @@
const selected = $datasources.selected === datasource._id const selected = $datasources.selected === datasource._id
const open = openDataSources.includes(datasource._id) const open = openDataSources.includes(datasource._id)
const containsSelected = containsActiveEntity(datasource) const containsSelected = containsActiveEntity(datasource)
const onlySource = $datasources.list.length === 1
return { return {
...datasource, ...datasource,
selected, selected,
open: selected || open || containsSelected, open: selected || open || containsSelected || onlySource,
} }
}) })
: [] : []

View File

@ -1,6 +1,6 @@
<script> <script>
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher, getContext } from "svelte"
export let icon export let icon
export let withArrow = false export let withArrow = false
@ -14,29 +14,46 @@
export let iconText export let iconText
export let iconColor export let iconColor
const scrollApi = getContext("scroll")
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
function onIconClick(event) { let contentRef
event.stopPropagation() $: selected && contentRef && scrollToView()
const onClick = () => {
scrollToView()
dispatch("click")
}
const onIconClick = e => {
e.stopPropagation()
dispatch("iconClick") dispatch("iconClick")
} }
const scrollToView = () => {
if (!scrollApi || !contentRef) {
return
}
const bounds = contentRef.getBoundingClientRect()
scrollApi.scrollTo(bounds)
}
</script> </script>
<div <div
class="nav-item" class="nav-item"
class:border class:border
class:selected class:selected
style={`padding-left: ${indentLevel * 14}px`} style={`padding-left: ${20 + indentLevel * 14}px`}
{draggable} {draggable}
on:dragend on:dragend
on:dragstart on:dragstart
on:dragover on:dragover
on:drop on:drop
on:click on:click={onClick}
ondragover="return false" ondragover="return false"
ondragenter="return false" ondragenter="return false"
> >
<div class="content"> <div class="nav-item-content" bind:this={contentRef}>
{#if withArrow} {#if withArrow}
<div class:opened class="icon arrow" on:click={onIconClick}> <div class:opened class="icon arrow" on:click={onIconClick}>
<Icon size="S" name="ChevronRight" /> <Icon size="S" name="ChevronRight" />
@ -64,11 +81,16 @@
<style> <style>
.nav-item { .nav-item {
border-radius: var(--border-radius-s);
cursor: pointer; cursor: pointer;
color: var(--grey-7); color: var(--grey-7);
transition: background-color transition: background-color
var(--spectrum-global-animation-duration-100, 130ms) ease-in-out; 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 { .nav-item.selected {
background-color: var(--grey-2); background-color: var(--grey-2);
@ -81,14 +103,14 @@
visibility: visible; visibility: visible;
} }
.content { .nav-item-content {
padding: 0 var(--spacing-s); flex: 1 1 auto;
height: 32px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
gap: var(--spacing-xs); gap: var(--spacing-xs);
width: max-content;
} }
.icon { .icon {
@ -111,12 +133,13 @@
} }
.text { .text {
flex: 1 1 auto;
font-weight: 600; font-weight: 600;
font-size: var(--spectrum-global-dimension-font-size-75); font-size: var(--spectrum-global-dimension-font-size-75);
white-space: nowrap;
max-width: 160px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; flex: 0 0 auto;
} }
.actions { .actions {
@ -125,9 +148,9 @@
height: 20px; height: 20px;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
flex-direction: row; display: grid;
justify-content: center; margin-left: var(--spacing-s);
align-items: center; place-items: center;
} }
.iconText { .iconText {

View File

@ -138,3 +138,10 @@
onOk={deleteComponent} onOk={deleteComponent}
/> />
{/if} {/if}
<style>
.icon {
display: grid;
place-items: center;
}
</style>

View File

@ -5,6 +5,7 @@
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { selectedComponentPath } from "builderStore"
export let components = [] export let components = []
export let currentComponent export let currentComponent
@ -71,10 +72,20 @@
notifications.error("Error saving component") 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> </script>
<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={() => selectComponent(component)}>
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE} {#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
<div <div
@ -97,12 +108,12 @@
withArrow withArrow
indentLevel={level + 1} indentLevel={level + 1}
selected={$store.selectedComponentId === component._id} selected={$store.selectedComponentId === component._id}
opened={!closedNodes[component._id] && component?._children?.length} opened={isOpen(component, $selectedComponentPath, closedNodes)}
> >
<ComponentDropdownMenu {component} /> <ComponentDropdownMenu {component} />
</NavItem> </NavItem>
{#if component._children && !closedNodes[component._id]} {#if isOpen(component, $selectedComponentPath, closedNodes)}
<svelte:self <svelte:self
components={component._children} components={component._children}
{currentComponent} {currentComponent}
@ -133,6 +144,10 @@
padding-left: 0; padding-left: 0;
margin: 0; margin: 0;
} }
ul,
li {
min-width: max-content;
}
.drop-item { .drop-item {
border-radius: var(--border-radius-m); border-radius: var(--border-radius-m);

View File

@ -65,3 +65,10 @@
<Input thin type="text" label="Name" bind:value={name} /> <Input thin type="text" label="Name" bind:value={name} />
</ModalContent> </ModalContent>
</Modal> </Modal>
<style>
.icon {
display: grid;
place-items: center;
}
</style>

View File

@ -75,4 +75,8 @@
align-items: flex-start; align-items: flex-start;
padding-left: var(--spacing-xl); padding-left: var(--spacing-xl);
} }
.icon {
display: grid;
place-items: center;
}
</style> </style>

View File

@ -29,6 +29,7 @@
export let border export let border
let routeManuallyOpened = false let routeManuallyOpened = false
$: selectedScreen = $currentAsset $: selectedScreen = $currentAsset
$: allScreens = getAllScreens(route) $: allScreens = getAllScreens(route)
$: filteredScreens = getFilteredScreens(allScreens, $screenSearchString) $: filteredScreens = getFilteredScreens(allScreens, $screenSearchString)
@ -83,7 +84,8 @@
<NavItem <NavItem
icon="WebPage" icon="WebPage"
indentLevel={indent || 1} indentLevel={indent || 1}
selected={$store.selectedScreenId === screen.id} selected={$store.selectedScreenId === screen.id &&
$store.currentView === "detail"}
opened={$store.selectedScreenId === screen.id} opened={$store.selectedScreenId === screen.id}
text={ROUTE_NAME_MAP[screen.route]?.[screen.role] || screen.route} text={ROUTE_NAME_MAP[screen.route]?.[screen.role] || screen.route}
withArrow={route.subpaths} withArrow={route.subpaths}

View File

@ -103,3 +103,10 @@
confirmText="Duplicate" confirmText="Duplicate"
/> />
</Modal> </Modal>
<style>
.icon {
display: grid;
place-items: center;
}
</style>

View File

@ -55,11 +55,10 @@
} }
</script> </script>
<div class="root"> <div class="root" class:has-screens={!!paths?.length}>
{#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}
<div class="empty"> <div class="empty">
There aren't any screens configured with this access role. There aren't any screens configured with this access role.
@ -68,9 +67,12 @@
</div> </div>
<style> <style>
.root.has-screens {
min-width: max-content;
}
div.empty { div.empty {
font-size: var(--font-size-xs); font-size: var(--font-size-s);
color: var(--grey-5); color: var(--grey-5);
padding-top: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-xl);
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<script> <script>
import { onMount } from "svelte" import { onMount, setContext } from "svelte"
import { goto, params } from "@roxi/routify" import { goto, params } from "@roxi/routify"
import { import {
store, store,
@ -18,11 +18,63 @@
Search, Search,
Tabs, Tabs,
Tab, Tab,
Layout as BBUILayout,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
export let showModal 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 = [ const tabs = [
{ {
title: "Screens", title: "Screens",
@ -79,7 +131,7 @@
<Tabs {selected} on:select={navigate}> <Tabs {selected} on:select={navigate}>
<Tab title="Screens"> <Tab title="Screens">
<div class="tab-content-padding"> <div class="tab-content-padding">
<div class="role-select"> <BBUILayout noPadding gap="XS">
<Select <Select
on:change={updateAccessRole} on:change={updateAccessRole}
value={$selectedAccessRole} value={$selectedAccessRole}
@ -93,17 +145,24 @@
label="Search Screens" label="Search Screens"
bind:value={$screenSearchString} bind:value={$screenSearchString}
/> />
</div> </BBUILayout>
<div class="nav-items-container"> <div class="nav-items-container" bind:this={scrollRef}>
<ComponentNavigationTree /> <ComponentNavigationTree />
</div> </div>
</div> </div>
</Tab> </Tab>
<Tab title="Layouts"> <Tab title="Layouts">
<div class="tab-content-padding"> <div class="tab-content-padding">
{#each $store.layouts as layout, idx (layout._id)} <div
<Layout {layout} border={idx > 0} /> class="nav-items-container nav-items-container--layouts"
{/each} 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}> <Modal bind:this={newLayoutModal}>
<NewLayoutModal /> <NewLayoutModal />
</Modal> </Modal>
@ -126,23 +185,45 @@
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
position: relative; 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 { .add-button {
position: absolute; position: absolute;
top: var(--spacing-l); top: var(--spacing-l);
right: var(--spacing-xl); right: var(--spacing-xl);
} }
.role-select { .tab-content-padding {
padding: 0 var(--spacing-xl);
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
margin-bottom: var(--spacing-m); gap: var(--spacing-xl);
gap: var(--spacing-m);
} }
.tab-content-padding { .nav-items-container {
padding: 0 var(--spacing-xl); 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> </style>

View File

@ -23,10 +23,8 @@
<div class="nav"> <div class="nav">
<Tabs {selected} on:select={selectFirstDatasource}> <Tabs {selected} on:select={selectFirstDatasource}>
<Tab title="Sources"> <Tab title="Sources">
<div class="tab-content-padding"> <DatasourceNavigator />
<DatasourceNavigator /> <CreateDatasourceModal bind:modal />
<CreateDatasourceModal bind:modal />
</div>
</Tab> </Tab>
</Tabs> </Tabs>
<div <div
@ -63,10 +61,6 @@
display: contents; display: contents;
} }
.tab-content-padding {
padding: 0 var(--spacing-xl);
}
.nav { .nav {
overflow-y: auto; overflow-y: auto;
background: var(--background); background: var(--background);

View File

@ -244,8 +244,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--spacing-l); gap: var(--spacing-l);
padding: 0 0 60px 0;
overflow-y: auto;
border-right: var(--border-light); border-right: var(--border-light);
} }

View File

@ -130,6 +130,7 @@
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
word-break: break-all;
} }
.button-container { .button-container {