Allow Collapsing Selected Components, Add Hotkeys for Collapsing Components (#12764)

* wip

* fix spelling

* wip

* linting

* change order of fix version of linting

* lint fix

* linting
This commit is contained in:
Gerard Burns 2024-02-15 10:53:58 +00:00 committed by GitHub
parent 23568502eb
commit b12aa639d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 178 additions and 27 deletions

View File

@ -58,7 +58,7 @@
"lint": "yarn run lint:eslint && yarn run lint:prettier",
"lint:fix:eslint": "eslint --fix --max-warnings=0 packages qa-core",
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"lint:fix": "yarn run lint:fix:eslint && yarn run lint:fix:prettier",
"build:specs": "lerna run --stream specs",
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
"build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild",

View File

@ -39,6 +39,7 @@ import { makePropSafe as safe } from "@budibase/string-templates"
import { getComponentFieldOptions } from "helpers/formFields"
import { createBuilderWebsocket } from "builderStore/websocket"
import { BuilderSocketEvent } from "@budibase/shared-core"
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
const INITIAL_FRONTEND_STATE = {
initialised: false,
@ -1053,6 +1054,7 @@ export const getFrontendStore = () => {
const screen = get(selectedScreen)
const parent = findComponentParent(screen.props, componentId)
const index = parent?._children.findIndex(x => x._id === componentId)
const componentTreeNodes = get(componentTreeNodesStore)
// Check for screen and navigation component edge cases
const screenComponentId = `${screen._id}-screen`
@ -1071,9 +1073,15 @@ export const getFrontendStore = () => {
if (index > 0) {
// If sibling before us accepts children, select a descendant
const previousSibling = parent._children[index - 1]
if (previousSibling._children?.length) {
if (
previousSibling._children?.length &&
componentTreeNodes[`nodeOpen-${previousSibling._id}`]
) {
let target = previousSibling
while (target._children?.length) {
while (
target._children?.length &&
componentTreeNodes[`nodeOpen-${target._id}`]
) {
target = target._children[target._children.length - 1]
}
return target._id
@ -1093,6 +1101,7 @@ export const getFrontendStore = () => {
const screen = get(selectedScreen)
const parent = findComponentParent(screen.props, componentId)
const index = parent?._children.findIndex(x => x._id === componentId)
const componentTreeNodes = get(componentTreeNodesStore)
// Check for screen and navigation component edge cases
const screenComponentId = `${screen._id}-screen`
@ -1102,7 +1111,11 @@ export const getFrontendStore = () => {
}
// If we have children, select first child
if (component._children?.length) {
if (
component._children?.length &&
(state.selectedComponentId === navComponentId ||
componentTreeNodes[`nodeOpen-${component._id}`])
) {
return component._children[0]._id
} else if (!parent) {
return null

View File

@ -3,6 +3,7 @@
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
export let component
export let opened
$: definition = componentStore.getDefinition(component?._component)
$: noPaste = !$componentStore.componentToPaste
@ -85,6 +86,39 @@
>
Paste
</MenuItem>
{#if component?._children?.length}
<MenuItem
icon="TreeExpand"
keyBind="!ArrowRight"
on:click={() => keyboardEvent("ArrowRight", false)}
disabled={opened}
>
Expand
</MenuItem>
<MenuItem
icon="TreeCollapse"
keyBind="!ArrowLeft"
on:click={() => keyboardEvent("ArrowLeft", false)}
disabled={!opened}
>
Collapse
</MenuItem>
<MenuItem
icon="TreeExpandAll"
keyBind="Ctrl+!ArrowRight"
on:click={() => keyboardEvent("ArrowRight", true)}
>
Expand All
</MenuItem>
<MenuItem
icon="TreeCollapseAll"
keyBind="Ctrl+!ArrowLeft"
on:click={() => keyboardEvent("ArrowLeft", true)}
>
Collapse All
</MenuItem>
{/if}
</ActionMenu>
<style>

View File

@ -9,6 +9,7 @@
import { goto, isActive } from "@roxi/routify"
import { notifications } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
let confirmDeleteDialog
let confirmEjectDialog
@ -61,6 +62,40 @@
["ArrowDown"]: () => {
componentStore.selectNext()
},
["ArrowRight"]: component => {
componentTreeNodesStore.expandNode(component._id)
},
["ArrowLeft"]: component => {
componentTreeNodesStore.collapseNode(component._id)
},
["Ctrl+ArrowRight"]: component => {
componentTreeNodesStore.expandNode(component._id)
const expandChildren = component => {
const children = component._children ?? []
children.forEach(child => {
componentTreeNodesStore.expandNode(child._id)
expandChildren(child)
})
}
expandChildren(component)
},
["Ctrl+ArrowLeft"]: component => {
componentTreeNodesStore.collapseNode(component._id)
const collapseChildren = component => {
const children = component._children ?? []
children.forEach(child => {
componentTreeNodesStore.collapseNode(child._id)
collapseChildren(child)
})
}
collapseChildren(component)
},
["Escape"]: () => {
if ($isActive(`./:componentId/new`)) {
$goto(`./${$componentStore.selectedComponentId}`)

View File

@ -17,11 +17,12 @@
} from "helpers/components"
import { get } from "svelte/store"
import { dndStore } from "./dndStore"
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
export let components = []
export let level = 0
let closedNodes = {}
$: openNodes = $componentTreeNodesStore
$: filteredComponents = components?.filter(component => {
return (
@ -54,15 +55,6 @@
return componentSupportsChildren(component) && component._children?.length
}
function toggleNodeOpen(componentId) {
if (closedNodes[componentId]) {
delete closedNodes[componentId]
} else {
closedNodes[componentId] = true
}
closedNodes = closedNodes
}
const onDrop = async e => {
e.stopPropagation()
try {
@ -72,14 +64,14 @@
}
}
const isOpen = (component, selectedComponentPath, closedNodes) => {
const isOpen = (component, selectedComponentPath, openNodes) => {
if (!component?._children?.length) {
return false
}
if (selectedComponentPath.includes(component._id)) {
if (selectedComponentPath.slice(0, -1).includes(component._id)) {
return true
}
return !closedNodes[component._id]
return openNodes[`nodeOpen-${component._id}`]
}
const isChildOfSelectedComponent = component => {
@ -96,7 +88,7 @@
<ul>
{#each filteredComponents || [] as component, index (component._id)}
{@const opened = isOpen(component, $selectedComponentPath, closedNodes)}
{@const opened = isOpen(component, $selectedComponentPath, openNodes)}
<li
on:click|stopPropagation={() => {
componentStore.select(component._id)
@ -110,7 +102,7 @@
on:dragend={dndStore.actions.reset}
on:dragstart={() => dndStore.actions.dragstart(component)}
on:dragover={dragover(component, index)}
on:iconClick={() => toggleNodeOpen(component._id)}
on:iconClick={() => componentTreeNodesStore.toggleNode(component._id)}
on:drop={onDrop}
hovering={$hoverStore.componentId === component._id}
on:mouseenter={() => hover(component._id)}
@ -125,8 +117,9 @@
highlighted={isChildOfSelectedComponent(component)}
selectedBy={$userSelectedResourceMap[component._id]}
>
<ComponentDropdownMenu {component} />
<ComponentDropdownMenu {opened} {component} />
</NavItem>
{#if opened}
<svelte:self
components={component._children}
@ -144,13 +137,6 @@
padding-left: 0;
margin: 0;
}
ul :global(.icon.arrow) {
transition: opacity 130ms ease-out;
opacity: 0;
}
ul:hover :global(.icon.arrow) {
opacity: 1;
}
ul,
li {
min-width: max-content;

View File

@ -0,0 +1,36 @@
import { createSessionStorageStore } from "@budibase/frontend-core"
const baseStore = createSessionStorageStore("openNodes", {})
const toggleNode = componentId => {
baseStore.update(openNodes => {
openNodes[`nodeOpen-${componentId}`] = !openNodes[`nodeOpen-${componentId}`]
return openNodes
})
}
const expandNode = componentId => {
baseStore.update(openNodes => {
openNodes[`nodeOpen-${componentId}`] = true
return openNodes
})
}
const collapseNode = componentId => {
baseStore.update(openNodes => {
openNodes[`nodeOpen-${componentId}`] = false
return openNodes
})
}
const store = {
subscribe: baseStore.subscribe,
toggleNode,
expandNode,
collapseNode,
}
export default store

View File

@ -1 +1,2 @@
export { createLocalStorageStore } from "./localStorage"
export { createSessionStorageStore } from "./sessionStorage"

View File

@ -0,0 +1,46 @@
import { get, writable } from "svelte/store"
export const createSessionStorageStore = (sessionStorageKey, initialValue) => {
const store = writable(initialValue, () => {
// Hydrate from session storage when we get a new subscriber
hydrate()
// Listen for session storage changes and keep store in sync
const storageListener = ({ key }) => {
return key === sessionStorageKey && hydrate()
}
window.addEventListener("storage", storageListener)
return () => window.removeEventListener("storage", storageListener)
})
// New store setter which updates the store and sessionstorage
const set = value => {
store.set(value)
sessionStorage.setItem(sessionStorageKey, JSON.stringify(value))
}
// New store updater which updates the store and sessionstorage
const update = updaterFn => set(updaterFn(get(store)))
// Hydrates the store from sessionstorage
const hydrate = () => {
const sessionValue = sessionStorage.getItem(sessionStorageKey)
if (sessionValue == null) {
set(initialValue)
} else {
try {
store.set(JSON.parse(sessionValue))
} catch {
set(initialValue)
}
}
}
// Patch the default svelte store functions with our overrides
return {
...store,
set,
update,
}
}