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:
parent
23568502eb
commit
b12aa639d3
|
@ -58,7 +58,7 @@
|
||||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||||
"lint:fix:eslint": "eslint --fix --max-warnings=0 packages qa-core",
|
"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: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:specs": "lerna run --stream specs",
|
||||||
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||||
"build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild",
|
"build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||||
|
|
|
@ -39,6 +39,7 @@ import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { getComponentFieldOptions } from "helpers/formFields"
|
import { getComponentFieldOptions } from "helpers/formFields"
|
||||||
import { createBuilderWebsocket } from "builderStore/websocket"
|
import { createBuilderWebsocket } from "builderStore/websocket"
|
||||||
import { BuilderSocketEvent } from "@budibase/shared-core"
|
import { BuilderSocketEvent } from "@budibase/shared-core"
|
||||||
|
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
|
||||||
|
|
||||||
const INITIAL_FRONTEND_STATE = {
|
const INITIAL_FRONTEND_STATE = {
|
||||||
initialised: false,
|
initialised: false,
|
||||||
|
@ -1053,6 +1054,7 @@ export const getFrontendStore = () => {
|
||||||
const screen = get(selectedScreen)
|
const screen = get(selectedScreen)
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
const index = parent?._children.findIndex(x => x._id === componentId)
|
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||||
|
const componentTreeNodes = get(componentTreeNodesStore)
|
||||||
|
|
||||||
// Check for screen and navigation component edge cases
|
// Check for screen and navigation component edge cases
|
||||||
const screenComponentId = `${screen._id}-screen`
|
const screenComponentId = `${screen._id}-screen`
|
||||||
|
@ -1071,9 +1073,15 @@ export const getFrontendStore = () => {
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
// If sibling before us accepts children, select a descendant
|
// If sibling before us accepts children, select a descendant
|
||||||
const previousSibling = parent._children[index - 1]
|
const previousSibling = parent._children[index - 1]
|
||||||
if (previousSibling._children?.length) {
|
if (
|
||||||
|
previousSibling._children?.length &&
|
||||||
|
componentTreeNodes[`nodeOpen-${previousSibling._id}`]
|
||||||
|
) {
|
||||||
let target = previousSibling
|
let target = previousSibling
|
||||||
while (target._children?.length) {
|
while (
|
||||||
|
target._children?.length &&
|
||||||
|
componentTreeNodes[`nodeOpen-${target._id}`]
|
||||||
|
) {
|
||||||
target = target._children[target._children.length - 1]
|
target = target._children[target._children.length - 1]
|
||||||
}
|
}
|
||||||
return target._id
|
return target._id
|
||||||
|
@ -1093,6 +1101,7 @@ export const getFrontendStore = () => {
|
||||||
const screen = get(selectedScreen)
|
const screen = get(selectedScreen)
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
const index = parent?._children.findIndex(x => x._id === componentId)
|
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||||
|
const componentTreeNodes = get(componentTreeNodesStore)
|
||||||
|
|
||||||
// Check for screen and navigation component edge cases
|
// Check for screen and navigation component edge cases
|
||||||
const screenComponentId = `${screen._id}-screen`
|
const screenComponentId = `${screen._id}-screen`
|
||||||
|
@ -1102,7 +1111,11 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have children, select first child
|
// 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
|
return component._children[0]._id
|
||||||
} else if (!parent) {
|
} else if (!parent) {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
|
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
|
||||||
|
|
||||||
export let component
|
export let component
|
||||||
|
export let opened
|
||||||
|
|
||||||
$: definition = componentStore.getDefinition(component?._component)
|
$: definition = componentStore.getDefinition(component?._component)
|
||||||
$: noPaste = !$componentStore.componentToPaste
|
$: noPaste = !$componentStore.componentToPaste
|
||||||
|
@ -85,6 +86,39 @@
|
||||||
>
|
>
|
||||||
Paste
|
Paste
|
||||||
</MenuItem>
|
</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>
|
</ActionMenu>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import { goto, isActive } from "@roxi/routify"
|
import { goto, isActive } from "@roxi/routify"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
|
||||||
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
let confirmEjectDialog
|
let confirmEjectDialog
|
||||||
|
@ -61,6 +62,40 @@
|
||||||
["ArrowDown"]: () => {
|
["ArrowDown"]: () => {
|
||||||
componentStore.selectNext()
|
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"]: () => {
|
["Escape"]: () => {
|
||||||
if ($isActive(`./:componentId/new`)) {
|
if ($isActive(`./:componentId/new`)) {
|
||||||
$goto(`./${$componentStore.selectedComponentId}`)
|
$goto(`./${$componentStore.selectedComponentId}`)
|
||||||
|
|
|
@ -17,11 +17,12 @@
|
||||||
} from "helpers/components"
|
} from "helpers/components"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { dndStore } from "./dndStore"
|
import { dndStore } from "./dndStore"
|
||||||
|
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
|
||||||
|
|
||||||
export let components = []
|
export let components = []
|
||||||
export let level = 0
|
export let level = 0
|
||||||
|
|
||||||
let closedNodes = {}
|
$: openNodes = $componentTreeNodesStore
|
||||||
|
|
||||||
$: filteredComponents = components?.filter(component => {
|
$: filteredComponents = components?.filter(component => {
|
||||||
return (
|
return (
|
||||||
|
@ -54,15 +55,6 @@
|
||||||
return componentSupportsChildren(component) && component._children?.length
|
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 => {
|
const onDrop = async e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
try {
|
try {
|
||||||
|
@ -72,14 +64,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOpen = (component, selectedComponentPath, closedNodes) => {
|
const isOpen = (component, selectedComponentPath, openNodes) => {
|
||||||
if (!component?._children?.length) {
|
if (!component?._children?.length) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (selectedComponentPath.includes(component._id)) {
|
if (selectedComponentPath.slice(0, -1).includes(component._id)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return !closedNodes[component._id]
|
return openNodes[`nodeOpen-${component._id}`]
|
||||||
}
|
}
|
||||||
|
|
||||||
const isChildOfSelectedComponent = component => {
|
const isChildOfSelectedComponent = component => {
|
||||||
|
@ -96,7 +88,7 @@
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{#each filteredComponents || [] as component, index (component._id)}
|
{#each filteredComponents || [] as component, index (component._id)}
|
||||||
{@const opened = isOpen(component, $selectedComponentPath, closedNodes)}
|
{@const opened = isOpen(component, $selectedComponentPath, openNodes)}
|
||||||
<li
|
<li
|
||||||
on:click|stopPropagation={() => {
|
on:click|stopPropagation={() => {
|
||||||
componentStore.select(component._id)
|
componentStore.select(component._id)
|
||||||
|
@ -110,7 +102,7 @@
|
||||||
on:dragend={dndStore.actions.reset}
|
on:dragend={dndStore.actions.reset}
|
||||||
on:dragstart={() => dndStore.actions.dragstart(component)}
|
on:dragstart={() => dndStore.actions.dragstart(component)}
|
||||||
on:dragover={dragover(component, index)}
|
on:dragover={dragover(component, index)}
|
||||||
on:iconClick={() => toggleNodeOpen(component._id)}
|
on:iconClick={() => componentTreeNodesStore.toggleNode(component._id)}
|
||||||
on:drop={onDrop}
|
on:drop={onDrop}
|
||||||
hovering={$hoverStore.componentId === component._id}
|
hovering={$hoverStore.componentId === component._id}
|
||||||
on:mouseenter={() => hover(component._id)}
|
on:mouseenter={() => hover(component._id)}
|
||||||
|
@ -125,8 +117,9 @@
|
||||||
highlighted={isChildOfSelectedComponent(component)}
|
highlighted={isChildOfSelectedComponent(component)}
|
||||||
selectedBy={$userSelectedResourceMap[component._id]}
|
selectedBy={$userSelectedResourceMap[component._id]}
|
||||||
>
|
>
|
||||||
<ComponentDropdownMenu {component} />
|
<ComponentDropdownMenu {opened} {component} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
|
||||||
{#if opened}
|
{#if opened}
|
||||||
<svelte:self
|
<svelte:self
|
||||||
components={component._children}
|
components={component._children}
|
||||||
|
@ -144,13 +137,6 @@
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
ul :global(.icon.arrow) {
|
|
||||||
transition: opacity 130ms ease-out;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
ul:hover :global(.icon.arrow) {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
ul,
|
ul,
|
||||||
li {
|
li {
|
||||||
min-width: max-content;
|
min-width: max-content;
|
||||||
|
|
|
@ -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
|
|
@ -1 +1,2 @@
|
||||||
export { createLocalStorageStore } from "./localStorage"
|
export { createLocalStorageStore } from "./localStorage"
|
||||||
|
export { createSessionStorageStore } from "./sessionStorage"
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue