From d15d034af36b43058b3fd75dff3909e35fd39e02 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 28 Jul 2022 14:25:58 +0100 Subject: [PATCH 01/21] Add keyboard shortcuts for components. Improve component reordering --- .../src/builderStore/store/frontend.js | 203 ++++++++++++++++-- .../[screenId]/_components/AppPreview.svelte | 4 + .../navigation/ComponentListPanel.svelte | 79 ++++++- .../components/preview/KeyboardManager.svelte | 10 +- packages/client/src/stores/builder.js | 4 +- 5 files changed, 266 insertions(+), 34 deletions(-) diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 58d803aa03..ff926bce1f 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -618,6 +618,16 @@ export const getFrontendStore = () => { } } + // Check inside is valid + if (mode === "inside") { + const definition = store.actions.components.getDefinition( + targetComponent._component + ) + if (!definition.hasChildren) { + mode = "below" + } + } + // Paste new component if (mode === "inside") { // Paste inside target component if chosen @@ -654,46 +664,197 @@ export const getFrontendStore = () => { return state }) }, + selectPrevious: () => { + const state = get(store) + const componentId = state.selectedComponentId + const screen = get(selectedScreen) + const parent = findComponentParent(screen.props, componentId) + let newComponentId = componentId + + // Check we aren't right at the top of the tree + const index = parent?._children.findIndex(x => x._id === componentId) + if (!parent || componentId === screen.props._id) { + return + } + + // If we have siblings above us, choose the sibling or a descendant + if (index > 0) { + // If sibling before us accepts children, select a descendant + const previousSibling = parent._children[index - 1] + if (previousSibling._children?.length) { + let target = previousSibling + while (target._children?.length) { + target = target._children[target._children.length - 1] + } + newComponentId = target._id + } + + // Otherwise just select sibling + else { + newComponentId = previousSibling._id + } + } + + // If no siblings above us, select the parent + else { + newComponentId = parent._id + } + + // Only update state if component changed + if (newComponentId !== componentId) { + store.update(state => { + state.selectedComponentId = newComponentId + return state + }) + } + }, + selectNext: () => { + const component = get(selectedComponent) + const componentId = component?._id + const screen = get(selectedScreen) + const parent = findComponentParent(screen.props, componentId) + const index = parent?._children.findIndex(x => x._id === componentId) + let newComponentId = componentId + + // If we have children, select first child + if (component._children?.length) { + newComponentId = component._children[0]._id + } else if (!parent) { + return null + } + + // Otherwise select the next sibling if we have one + else if (index < parent._children.length - 1) { + const nextSibling = parent._children[index + 1] + newComponentId = nextSibling._id + } + + // Last child, select our parents next sibling + else { + let target = parent + let targetParent = findComponentParent(screen.props, target._id) + let targetIndex = targetParent?._children.findIndex( + child => child._id === target._id + ) + while ( + targetParent != null && + targetIndex === targetParent._children?.length - 1 + ) { + target = targetParent + targetParent = findComponentParent(screen.props, target._id) + targetIndex = targetParent?._children.findIndex( + child => child._id === target._id + ) + } + if (targetParent) { + newComponentId = targetParent._children[targetIndex + 1]._id + } + } + + // Only update state if component ID is different + if (newComponentId !== componentId) { + store.update(state => { + state.selectedComponentId = newComponentId + return state + }) + } + }, moveUp: async component => { await store.actions.screens.patch(screen => { const componentId = component?._id const parent = findComponentParent(screen.props, componentId) - if (!parent?._children?.length) { - return false + + // Check we aren't right at the top of the tree + const index = parent?._children.findIndex(x => x._id === componentId) + if (!parent || (index === 0 && parent._id === screen.props._id)) { + return } - const currentIndex = parent._children.findIndex( - child => child._id === componentId - ) - if (currentIndex === 0) { - return false - } - const originalComponent = cloneDeep(parent._children[currentIndex]) - const newChildren = parent._children.filter( + + // Copy original component and remove it from the parent + const originalComponent = cloneDeep(parent._children[index]) + parent._children = parent._children.filter( component => component._id !== componentId ) - newChildren.splice(currentIndex - 1, 0, originalComponent) - parent._children = newChildren + + // If we have siblings above us, move up + if (index > 0) { + // If sibling before us accepts children, move to last child of + // sibling + const previousSibling = parent._children[index - 1] + const definition = store.actions.components.getDefinition( + previousSibling._component + ) + if (definition.hasChildren) { + previousSibling._children.push(originalComponent) + } + + // Otherwise just move component above sibling + else { + parent._children.splice(index - 1, 0, originalComponent) + } + } + + // If no siblings above us, go above the parent as long as it isn't + // the screen + else if (parent._id !== screen.props._id) { + const grandParent = findComponentParent(screen.props, parent._id) + const parentIndex = grandParent._children.findIndex( + child => child._id === parent._id + ) + grandParent._children.splice(parentIndex, 0, originalComponent) + } }) }, moveDown: async component => { await store.actions.screens.patch(screen => { const componentId = component?._id const parent = findComponentParent(screen.props, componentId) + + // Sanity check parent is found if (!parent?._children?.length) { return false } - const currentIndex = parent._children.findIndex( - child => child._id === componentId - ) - if (currentIndex === parent._children.length - 1) { - return false + + // Check we aren't right at the bottom of the tree + const index = parent._children.findIndex(x => x._id === componentId) + if ( + index === parent._children.length - 1 && + parent._id === screen.props._id + ) { + return } - const originalComponent = cloneDeep(parent._children[currentIndex]) - const newChildren = parent._children.filter( + + // Copy the original component and remove from parent + const originalComponent = cloneDeep(parent._children[index]) + parent._children = parent._children.filter( component => component._id !== componentId ) - newChildren.splice(currentIndex + 1, 0, originalComponent) - parent._children = newChildren + + // Move below the next sibling if we are not the last sibling + if (index < parent._children.length) { + // If the next sibling has children, become the first child + const nextSibling = parent._children[index] + const definition = store.actions.components.getDefinition( + nextSibling._component + ) + if (definition.hasChildren) { + nextSibling._children.splice(0, 0, originalComponent) + } + + // Otherwise move below next sibling + else { + parent._children.splice(index + 1, 0, originalComponent) + } + } + + // Last child, so move below our parent + else { + const grandParent = findComponentParent(screen.props, parent._id) + const parentIndex = grandParent._children.findIndex( + child => child._id === parent._id + ) + grandParent._children.splice(parentIndex + 1, 0, originalComponent) + } }) }, updateStyle: async (name, value) => { diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte index abb956c9d3..dc22f93300 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte @@ -144,7 +144,11 @@ } else if (type === "update-prop") { await store.actions.components.updateSetting(data.prop, data.value) } else if (type === "delete-component" && data.id) { + // Legacy type, can be deleted in future confirmDeleteComponent(data.id) + } else if (type === "key-down") { + const { key, ctrlKey } = data + document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey })) } else if (type === "duplicate-component" && data.id) { const rootComponent = get(currentAsset).props const component = findComponent(rootComponent, data.id) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentListPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentListPanel.svelte index 1bb4e3d9cd..e10015f964 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentListPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentListPanel.svelte @@ -2,16 +2,19 @@ import Panel from "components/design/Panel.svelte" import ComponentTree from "./ComponentTree.svelte" import { dndStore } from "./dndStore.js" - import { goto } from "@roxi/routify" - import { store, selectedScreen } from "builderStore" + import { goto, isActive } from "@roxi/routify" + import { store, selectedScreen, selectedComponent } from "builderStore" import NavItem from "components/common/NavItem.svelte" import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte" - import { setContext } from "svelte" + import { setContext, onMount } from "svelte" + import { get } from "svelte/store" import DNDPositionIndicator from "./DNDPositionIndicator.svelte" import { DropPosition } from "./dndStore" import { notifications } from "@budibase/bbui" + import ConfirmDialog from "components/common/ConfirmDialog.svelte" let scrollRef + let confirmDeleteDialog const scrollTo = bounds => { if (!bounds) { @@ -69,6 +72,69 @@ setContext("scroll", { scrollTo, }) + + const deleteComponent = async () => { + await store.actions.components.delete(get(selectedComponent)) + } + + const handleKeyPress = async e => { + // Ignore repeating events + if (e.repeat) { + return + } + // Ignore events when typing + const activeTag = document.activeElement?.tagName.toLowerCase() + if (["input", "textarea"].indexOf(activeTag) !== -1 && e.key !== "Escape") { + return + } + const component = get(selectedComponent) + try { + if (e.key === "Delete") { + e.preventDefault() + confirmDeleteDialog.show() + } else if (e.ctrlKey) { + if (e.key === "ArrowUp") { + e.preventDefault() + e.stopPropagation() + await store.actions.components.moveUp(component) + } else if (e.key === "ArrowDown") { + e.preventDefault() + await store.actions.components.moveDown(component) + } else if (e.key === "c") { + e.preventDefault() + await store.actions.components.copy(component, false) + } else if (e.key === "x") { + e.preventDefault() + store.actions.components.copy(component, true) + } else if (e.key === "v") { + e.preventDefault() + await store.actions.components.paste(component, "inside") + } else if (e.key === "Enter") { + e.preventDefault() + $goto("./new") + } + } else if (e.key === "ArrowUp") { + e.preventDefault() + await store.actions.components.selectPrevious() + } else if (e.key === "ArrowDown") { + e.preventDefault() + await store.actions.components.selectNext() + } else if (e.key === "Escape" && $isActive("./new")) { + e.preventDefault() + $goto("./") + } + } catch (error) { + console.log(error) + notifications.error("Error handling key press") + } + } + + onMount(() => { + document.addEventListener("keydown", handleKeyPress) + return () => { + document.removeEventListener("keydown", handleKeyPress) + } + }) + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/theme/_components/ButtonRoundnessSelect.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/theme/_components/ButtonRoundnessSelect.svelte new file mode 100644 index 0000000000..21b04f694f --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/theme/_components/ButtonRoundnessSelect.svelte @@ -0,0 +1,38 @@ + + +
+ +
+ +
+
+ + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/theme/_components/ThemeSettingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/theme/_components/ThemeSettingsPanel.svelte index 4bad3b7bc4..19184a8644 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/theme/_components/ThemeSettingsPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/theme/_components/ThemeSettingsPanel.svelte @@ -11,25 +11,7 @@ import { get } from "svelte/store" import { DefaultAppTheme } from "constants" import AppThemeSelect from "./AppThemeSelect.svelte" - - const ButtonBorderRadiusOptions = [ - { - label: "Square", - value: "0", - }, - { - label: "Soft edge", - value: "4px", - }, - { - label: "Curved", - value: "8px", - }, - { - label: "Round", - value: "16px", - }, - ] + import ButtonRoundnessSelect from "./ButtonRoundnessSelect.svelte" $: customTheme = $store.customTheme || {} @@ -52,22 +34,11 @@ - -
- {#each ButtonBorderRadiusOptions as option} -
- -
- {/each} -
+ + update("buttonBorderRadius", e.detail)} + />
@@ -88,29 +59,3 @@ - - From 5737e23dd6b7e5463a00316e7c6d7e8a8adaf8f3 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 29 Jul 2022 12:10:53 +0100 Subject: [PATCH 08/21] Add keybindings to component context menu. Add duplicate keybinding. Simplify pasting --- packages/bbui/src/Menu/Item.svelte | 13 +++++ .../navigation/ComponentDropdownMenu.svelte | 54 +++++++++++-------- .../navigation/ComponentListPanel.svelte | 4 ++ .../components/preview/KeyboardManager.svelte | 6 +++ 4 files changed, 55 insertions(+), 22 deletions(-) diff --git a/packages/bbui/src/Menu/Item.svelte b/packages/bbui/src/Menu/Item.svelte index a5609683a8..138eb33b35 100644 --- a/packages/bbui/src/Menu/Item.svelte +++ b/packages/bbui/src/Menu/Item.svelte @@ -8,6 +8,7 @@ export let icon = undefined export let disabled = undefined export let noClose = false + export let keyBind = undefined const onClick = () => { if (actionMenu && !noClose) { @@ -36,10 +37,22 @@ {/if} + {#if keyBind} +
+ {keyBind} +
+ {/if} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte index ed66c66c29..d2e35b8364 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte @@ -64,44 +64,54 @@
- + Delete - + Move up - + Move down - + Duplicate - storeComponentForCopy(true)}> + storeComponentForCopy(true)} + > Cut - storeComponentForCopy(false)}> + storeComponentForCopy(false)} + > Copy - pasteComponent("above")} - disabled={noPaste} - > - Paste above - pasteComponent("below")} + keyBind="Ctrl+V" + on:click={() => pasteComponent("inside")} disabled={noPaste} > - Paste below - - pasteComponent("inside")} - disabled={noPaste || noChildrenAllowed} - > - Paste inside + Paste From 65e43b0f70c65dc964d1d08d0d0169e00848d9ad Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 29 Jul 2022 13:05:50 +0100 Subject: [PATCH 09/21] Improve component selection after deleting a component --- .../src/builderStore/store/frontend.js | 108 +++++++++--------- 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index ff926bce1f..c7795d4b54 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -534,7 +534,16 @@ export const getFrontendStore = () => { if (!component) { return } - let parentId + + // Determine the next component to select after deletion + const state = get(store) + let nextSelectedComponentId + if (state.selectedComponentId === component._id) { + nextSelectedComponentId = store.actions.components.getNext() + if (!nextSelectedComponentId) { + nextSelectedComponentId = store.actions.components.getPrevious() + } + } // Patch screen await store.actions.screens.patch(screen => { @@ -549,17 +558,18 @@ export const getFrontendStore = () => { if (!parent) { return false } - parentId = parent._id parent._children = parent._children.filter( child => child._id !== component._id ) }) - // Select the deleted component's parent - store.update(state => { - state.selectedComponentId = parentId - return state - }) + // Update selected component if required + if (nextSelectedComponentId) { + store.update(state => { + state.selectedComponentId = nextSelectedComponentId + return state + }) + } }, copy: (component, cut = false, selectParent = true) => { // Update store with copied component @@ -664,17 +674,16 @@ export const getFrontendStore = () => { return state }) }, - selectPrevious: () => { + getPrevious: () => { const state = get(store) const componentId = state.selectedComponentId const screen = get(selectedScreen) const parent = findComponentParent(screen.props, componentId) - let newComponentId = componentId // Check we aren't right at the top of the tree const index = parent?._children.findIndex(x => x._id === componentId) if (!parent || componentId === screen.props._id) { - return + return null } // If we have siblings above us, choose the sibling or a descendant @@ -686,75 +695,72 @@ export const getFrontendStore = () => { while (target._children?.length) { target = target._children[target._children.length - 1] } - newComponentId = target._id + return target._id } // Otherwise just select sibling - else { - newComponentId = previousSibling._id - } + return previousSibling._id } // If no siblings above us, select the parent - else { - newComponentId = parent._id - } - - // Only update state if component changed - if (newComponentId !== componentId) { - store.update(state => { - state.selectedComponentId = newComponentId - return state - }) - } + return parent._id }, - selectNext: () => { + getNext: () => { const component = get(selectedComponent) const componentId = component?._id const screen = get(selectedScreen) const parent = findComponentParent(screen.props, componentId) const index = parent?._children.findIndex(x => x._id === componentId) - let newComponentId = componentId // If we have children, select first child if (component._children?.length) { - newComponentId = component._children[0]._id + return component._children[0]._id } else if (!parent) { return null } // Otherwise select the next sibling if we have one - else if (index < parent._children.length - 1) { + if (index < parent._children.length - 1) { const nextSibling = parent._children[index + 1] - newComponentId = nextSibling._id + return nextSibling._id } // Last child, select our parents next sibling - else { - let target = parent - let targetParent = findComponentParent(screen.props, target._id) - let targetIndex = targetParent?._children.findIndex( + let target = parent + let targetParent = findComponentParent(screen.props, target._id) + let targetIndex = targetParent?._children.findIndex( + child => child._id === target._id + ) + while ( + targetParent != null && + targetIndex === targetParent._children?.length - 1 + ) { + target = targetParent + targetParent = findComponentParent(screen.props, target._id) + targetIndex = targetParent?._children.findIndex( child => child._id === target._id ) - while ( - targetParent != null && - targetIndex === targetParent._children?.length - 1 - ) { - target = targetParent - targetParent = findComponentParent(screen.props, target._id) - targetIndex = targetParent?._children.findIndex( - child => child._id === target._id - ) - } - if (targetParent) { - newComponentId = targetParent._children[targetIndex + 1]._id - } } - - // Only update state if component ID is different - if (newComponentId !== componentId) { + if (targetParent) { + return targetParent._children[targetIndex + 1]._id + } else { + return null + } + }, + selectPrevious: () => { + const previousId = store.actions.components.getPrevious() + if (previousId) { store.update(state => { - state.selectedComponentId = newComponentId + state.selectedComponentId = previousId + return state + }) + } + }, + selectNext: () => { + const nextId = store.actions.components.getNext() + if (nextId) { + store.update(state => { + state.selectedComponentId = nextId return state }) } From cfe10e36e411550922bc70e2f9fe43fea7c9bd8d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 29 Jul 2022 13:10:02 +0100 Subject: [PATCH 10/21] Add key binds to screenslot component --- packages/bbui/src/Menu/Item.svelte | 3 +++ .../navigation/ScreenslotDropdownMenu.svelte | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/bbui/src/Menu/Item.svelte b/packages/bbui/src/Menu/Item.svelte index 138eb33b35..495bb8d71f 100644 --- a/packages/bbui/src/Menu/Item.svelte +++ b/packages/bbui/src/Menu/Item.svelte @@ -55,4 +55,7 @@ font-size: 12px; font-weight: 600; } + .is-disabled .keyBind { + color: var(--spectrum-global-color-gray-300); + } diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ScreenslotDropdownMenu.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ScreenslotDropdownMenu.svelte index 7ec7b0821c..9d4486096f 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ScreenslotDropdownMenu.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ScreenslotDropdownMenu.svelte @@ -31,15 +31,20 @@
- storeComponentForCopy(false)}> + storeComponentForCopy(false)} + > Copy pasteComponent("inside")} disabled={noPaste} > - Paste inside + Paste {/if} From 447f463ec5c13930ebdfd47a324dbb36d86c30a5 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 29 Jul 2022 13:11:50 +0100 Subject: [PATCH 11/21] Fix icon colors being static grey in automation section --- .../automation/AutomationBuilder/FlowChart/FlowItem.svelte | 2 +- .../AutomationBuilder/FlowChart/FlowItemHeader.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index c1618a890f..dd8fa7df46 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -162,7 +162,7 @@ width="28px" height="28px" class="spectrum-Icon" - style="color:grey;" + style="color:var(--spectrum-global-color-gray-700);" focusable="false" > diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte index 99c6f251f9..676af0d17c 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte @@ -63,7 +63,7 @@ width="28px" height="28px" class="spectrum-Icon" - style="color:grey;" + style="color:var(--spectrum-global-color-gray-700);" focusable="false" > From 808de3bbfbadceb46fe37a54eae3344ab1da5cd5 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 1 Aug 2022 09:54:30 +0100 Subject: [PATCH 12/21] Update modal background colours to be themeable and use theme colours for nord and midnight --- packages/bbui/src/Modal/Modal.svelte | 24 +++++++++++++------ .../frontend-core/src/themes/midnight.css | 2 ++ packages/frontend-core/src/themes/nord.css | 2 ++ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/bbui/src/Modal/Modal.svelte b/packages/bbui/src/Modal/Modal.svelte index 85a3d25e49..47420444a2 100644 --- a/packages/bbui/src/Modal/Modal.svelte +++ b/packages/bbui/src/Modal/Modal.svelte @@ -82,12 +82,12 @@ --> {#if visible} -
+
+