From 2db40c6d2c6178bea6ba91381dfb2e83973f8fa3 Mon Sep 17 00:00:00 2001 From: Michael Shanks Date: Wed, 12 Aug 2020 16:28:19 +0100 Subject: [PATCH 1/7] dnd component nav --- .../builder/src/builderStore/store/index.js | 53 +++++++- .../builder/src/builderStore/storeUtils.js | 6 + .../ComponentDropdownMenu.svelte | 46 +------ .../userInterface/ComponentsHierarchy.svelte | 19 +-- .../ComponentsHierarchyChildren.svelte | 113 +++++++++++++++++- 5 files changed, 185 insertions(+), 52 deletions(-) diff --git a/packages/builder/src/builderStore/store/index.js b/packages/builder/src/builderStore/store/index.js index 5b78ea0979..3894725ead 100644 --- a/packages/builder/src/builderStore/store/index.js +++ b/packages/builder/src/builderStore/store/index.js @@ -1,4 +1,4 @@ -import { values } from "lodash/fp" +import { values, cloneDeep } from "lodash/fp" import { get_capitalised_name } from "../../helpers" import { backendUiStore } from "builderStore" import * as backendStoreActions from "./backend" @@ -24,8 +24,8 @@ import { saveCurrentPreviewItem as _saveCurrentPreviewItem, saveScreenApi as _saveScreenApi, regenerateCssForCurrentScreen, + generateNewIdsForComponent, } from "../storeUtils" - export const getStore = () => { const initial = { apps: [], @@ -70,6 +70,8 @@ export const getStore = () => { store.addTemplatedComponent = addTemplatedComponent(store) store.setMetadataProp = setMetadataProp(store) store.editPageOrScreen = editPageOrScreen(store) + store.pasteComponent = pasteComponent(store) + store.storeComponentForCopy = storeComponentForCopy(store) return store } @@ -454,3 +456,50 @@ const setMetadataProp = store => (name, prop) => { return s }) } + +const storeComponentForCopy = store => (component, cut = false) => { + store.update(s => { + const copiedComponent = cloneDeep(component) + s.componentToPaste = copiedComponent + s.componentToPaste.isCut = cut + if (cut) { + const parent = getParent(s.currentPreviewItem.props, component._id) + parent._children = parent._children.filter(c => c._id !== component._id) + selectComponent(s, parent) + } + + return s + }) +} + +const pasteComponent = store => (targetComponent, mode) => { + store.update(s => { + if (!s.componentToPaste) return s + + const componentToPaste = cloneDeep(s.componentToPaste) + // retain the same ids as things may be referencing this component + if (componentToPaste.isCut) { + // in case we paste a second time + s.componentToPaste.isCut = false + } else { + generateNewIdsForComponent(componentToPaste) + } + delete componentToPaste.isCut + + if (mode === "inside") { + targetComponent._children.push(componentToPaste) + return s + } + + const parent = getParent(s.currentPreviewItem.props, targetComponent) + + const targetIndex = parent._children.indexOf(targetComponent) + const index = mode === "above" ? targetIndex : targetIndex + 1 + parent._children.splice(index, 0, cloneDeep(componentToPaste)) + regenerateCssForCurrentScreen(s) + _saveCurrentPreviewItem(s) + selectComponent(s, componentToPaste) + + return s + }) +} diff --git a/packages/builder/src/builderStore/storeUtils.js b/packages/builder/src/builderStore/storeUtils.js index 63fd7ecd00..1630d118e0 100644 --- a/packages/builder/src/builderStore/storeUtils.js +++ b/packages/builder/src/builderStore/storeUtils.js @@ -1,6 +1,7 @@ import { makePropsSafe } from "components/userInterface/pagesParsing/createProps" import api from "./api" import { generate_screen_css } from "./generate_css" +import { uuid } from "./uuid" export const selectComponent = (state, component) => { const componentDef = component._component.startsWith("##") @@ -79,3 +80,8 @@ export const regenerateCssForCurrentScreen = state => { ]) return state } + +export const generateNewIdsForComponent = c => + walkProps(c, p => { + p._id = uuid() + }) diff --git a/packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte b/packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte index 28dd59c16f..23e28a7b48 100644 --- a/packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte +++ b/packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte @@ -28,8 +28,7 @@ }) $: dropdown && UIkit.util.on(dropdown, "shown", () => (hidden = false)) $: noChildrenAllowed = - !component || - getComponentDefinition($store, component._component).children === false + !component || !getComponentDefinition($store, component._component).children $: noPaste = !$store.componentToPaste const lastPartOfName = c => (c ? last(c._component.split("/")) : "") @@ -105,49 +104,14 @@ }) } - const generateNewIdsForComponent = c => - walkProps(c, p => { - p._id = uuid() - }) - const storeComponentForCopy = (cut = false) => { - store.update(s => { - const copiedComponent = cloneDeep(component) - s.componentToPaste = copiedComponent - if (cut) { - const parent = getParent(s.currentPreviewItem.props, component._id) - parent._children = parent._children.filter(c => c._id !== component._id) - selectComponent(s, parent) - } - - return s - }) + // lives in store - also used by drag drop + store.storeComponentForCopy(component, cut) } const pasteComponent = mode => { - store.update(s => { - if (!s.componentToPaste) return s - - const componentToPaste = cloneDeep(s.componentToPaste) - generateNewIdsForComponent(componentToPaste) - delete componentToPaste._cutId - - if (mode === "inside") { - component._children.push(componentToPaste) - return s - } - - const parent = getParent(s.currentPreviewItem.props, component) - - const targetIndex = parent._children.indexOf(component) - const index = mode === "above" ? targetIndex : targetIndex + 1 - parent._children.splice(index, 0, cloneDeep(componentToPaste)) - regenerateCssForCurrentScreen(s) - saveCurrentPreviewItem(s) - selectComponent(s, componentToPaste) - - return s - }) + // lives in store - also used by drag drop + store.pasteComponent(component, mode) } diff --git a/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte b/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte index b25d7a4e7b..db4ea2fc27 100644 --- a/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte +++ b/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte @@ -7,6 +7,7 @@ import { store } from "builderStore" import { ArrowDownIcon, ShapeIcon } from "components/common/Icons/" import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte" + import { writable } from "svelte/store" export let screens = [] @@ -16,12 +17,15 @@ const joinPath = join("/") const normalizedName = name => - pipe(name, [ - trimCharsStart("./"), - trimCharsStart("~/"), - trimCharsStart("../"), - trimChars(" "), - ]) + pipe( + name, + [ + trimCharsStart("./"), + trimCharsStart("~/"), + trimCharsStart("../"), + trimChars(" "), + ] + ) const changeScreen = screen => { store.setCurrentScreen(screen.props._instanceName) @@ -57,7 +61,8 @@ {#if $store.currentPreviewItem.props._instanceName && $store.currentPreviewItem.props._instanceName === screen.props._instanceName && screen.props._children} + currentComponent={$store.currentComponentInfo} + dragDropStore={writable({})} /> {/if} {/each} diff --git a/packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte b/packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte index 691d188b07..7d24a42a8e 100644 --- a/packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte +++ b/packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte @@ -15,11 +15,19 @@ export let currentComponent export let onSelect = () => {} export let level = 0 + export let dragDropStore + + let dropUnderComponent + let componentToDrop const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1) const get_name = s => (!s ? "" : last(s.split("/"))) - const get_capitalised_name = name => pipe(name, [get_name, capitalise]) + const get_capitalised_name = name => + pipe( + name, + [get_name, capitalise] + ) const isScreenslot = name => name === "##builtin/screenslot" const selectComponent = component => { @@ -32,15 +40,92 @@ // Go to correct URL $goto(`./:page/:screen/${path}`) } + + const dragstart = component => e => { + e.dataTransfer.dropEffect = "move" + dragDropStore.update(s => { + s.componentToDrop = component + return s + }) + } + + const dragover = (component, index) => e => { + const canHaveChildrenButIsEmpty = + $store.components[component._component].children && + component._children.length === 0 + + e.dataTransfer.dropEffect = "copy" + dragDropStore.update(s => { + const isBottomHalf = e.offsetY > e.currentTarget.offsetHeight / 2 + s.targetComponent = component + // only allow dropping inside when container type + // is empty. If it has children, the user can drag over + // it's existing children + if (canHaveChildrenButIsEmpty) { + if (index === 0) { + // when its the first component in the screen, + // we divide into 3, so we can paste above, inside or below + const pos = e.offsetY / e.currentTarget.offsetHeight + if (pos < 0.4) { + s.dropPosition = "above" + } else if (pos > 0.8) { + // purposely giving this the least space as it is often covered + // by the component below's "above" space + s.dropPosition = "below" + } else { + s.dropPosition = "inside" + } + } else { + s.dropPosition = isBottomHalf ? "below" : "inside" + } + } else { + s.dropPosition = isBottomHalf ? "below" : "above" + } + return s + }) + return false + } + + const drop = () => { + if ($dragDropStore.targetComponent !== $dragDropStore.componentToDrop) { + store.storeComponentForCopy($dragDropStore.componentToDrop, true) + store.pasteComponent( + $dragDropStore.targetComponent, + $dragDropStore.dropPosition + ) + } + dragDropStore.update(s => { + s.dropPosition = "" + s.targetComponent = null + s.componentToDrop = null + return s + }) + } @@ -78,6 +182,11 @@ align-items: center; } + .drop-item { + background: var(--blue-light); + height: 36px; + } + .actions { display: none; height: 24px; From c95363dd66eced88427319ed8dfb21255b52b60a Mon Sep 17 00:00:00 2001 From: Michael Shanks Date: Wed, 12 Aug 2020 16:28:41 +0100 Subject: [PATCH 2/7] container components need `children: true` --- .../components/userInterface/pagesParsing/createProps.js | 2 +- packages/builder/tests/createProps.spec.js | 7 +++---- packages/standard-components/components.json | 3 +++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/components/userInterface/pagesParsing/createProps.js b/packages/builder/src/components/userInterface/pagesParsing/createProps.js index de4de1c2a6..7e213f82df 100644 --- a/packages/builder/src/components/userInterface/pagesParsing/createProps.js +++ b/packages/builder/src/components/userInterface/pagesParsing/createProps.js @@ -48,7 +48,7 @@ export const createProps = (componentDefinition, derivedFromProps) => { assign(props, derivedFromProps) } - if (componentDefinition.children !== false && isUndefined(props._children)) { + if (componentDefinition.children && isUndefined(props._children)) { props._children = [] } diff --git a/packages/builder/tests/createProps.spec.js b/packages/builder/tests/createProps.spec.js index bd468ba41a..3759598c1a 100644 --- a/packages/builder/tests/createProps.spec.js +++ b/packages/builder/tests/createProps.spec.js @@ -18,7 +18,7 @@ describe("createDefaultProps", () => { expect(props.fieldName).toBeDefined() expect(props.fieldName).toBe("something") stripStandardProps(props) - expect(keys(props).length).toBe(3) + expect(keys(props).length).toBe(2) }) it("should set component _component", () => { @@ -104,14 +104,13 @@ describe("createDefaultProps", () => { expect(props._children).toEqual([]) }) - it("should create a _children array when children not defined ", () => { + it("should not create a _children array when children not defined ", () => { const comp = getcomponent() const { props, errors } = createProps(comp) expect(errors).toEqual([]) - expect(props._children).toBeDefined() - expect(props._children).toEqual([]) + expect(props._children).not.toBeDefined() }) it("should not create _children array when children=false ", () => { diff --git a/packages/standard-components/components.json b/packages/standard-components/components.json index 6836f4981c..ad898accfc 100644 --- a/packages/standard-components/components.json +++ b/packages/standard-components/components.json @@ -244,6 +244,7 @@ }, "list": { "description": "A configurable data list that attaches to your backend models.", + "children": true, "data": true, "props": { "model": "models" @@ -263,6 +264,7 @@ }, "recorddetail": { "description": "Loads a record, using an ID in the url", + "children": true, "data": true, "props": { "model": "models" @@ -599,6 +601,7 @@ }, "container": { "name": "Container", + "children": true, "description": "An element that contains and lays out other elements. e.g.
,
etc", "props": { "className": "string", From 3703aa2a1e748cf3f2e736abb1f280ce166189e3 Mon Sep 17 00:00:00 2001 From: Michael Shanks Date: Wed, 12 Aug 2020 17:00:05 +0100 Subject: [PATCH 3/7] auto nesting or not, on add component --- packages/builder/src/builderStore/store/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/builder/src/builderStore/store/index.js b/packages/builder/src/builderStore/store/index.js index 3894725ead..f5849c480a 100644 --- a/packages/builder/src/builderStore/store/index.js +++ b/packages/builder/src/builderStore/store/index.js @@ -293,9 +293,14 @@ const addChildComponent = store => (componentToAdd, presetProps = {}) => { state ) - state.currentComponentInfo._children = state.currentComponentInfo._children.concat( - newComponent.props - ) + const currentComponent = + state.components[state.currentComponentInfo._component] + + const targetParent = currentComponent.children + ? state.currentComponentInfo + : getParent(state.currentPreviewItem.props, state.currentComponentInfo) + + targetParent._children = targetParent._children.concat(newComponent.props) state.currentFrontEndType === "page" ? _savePage(state) From 0e9ecccd911b9a36b7fbfe94c21654f63c265a6b Mon Sep 17 00:00:00 2001 From: Michael Shanks Date: Thu, 13 Aug 2020 10:15:09 +0100 Subject: [PATCH 4/7] changes from code review --- .../userInterface/ComponentsHierarchy.svelte | 4 +++- .../ComponentsHierarchyChildren.svelte | 15 +++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte b/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte index db4ea2fc27..b7fc60f347 100644 --- a/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte +++ b/packages/builder/src/components/userInterface/ComponentsHierarchy.svelte @@ -11,6 +11,8 @@ export let screens = [] + const dragDropStore = writable({}) + let confirmDeleteDialog let componentToDelete = "" @@ -62,7 +64,7 @@ + {dragDropStore} /> {/if} {/each} diff --git a/packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte b/packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte index 7d24a42a8e..e23d34bc3c 100644 --- a/packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte +++ b/packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte @@ -107,7 +107,7 @@ {#each components as component, index (component._id)}
  • selectComponent(component)}> - {#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition == 'above'} + {#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition === 'above'}
    {/if} - {#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition == 'inside'} + {#if $dragDropStore && $dragDropStore.targetComponent === component && ($dragDropStore.dropPosition === 'inside' || $dragDropStore.dropPosition === 'below')}
    - {/if} - - {#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition == 'below'} -
    + style="margin-left: {(level + ($dragDropStore.dropPosition === 'inside' ? 2 : 0)) * 20 + 40}px" /> {/if}
  • {/each} From 180fe1801f9fc3720b2c0f81cf6ee305b4bdf5f3 Mon Sep 17 00:00:00 2001 From: Michael Shanks Date: Thu, 13 Aug 2020 10:15:37 +0100 Subject: [PATCH 5/7] fix: added dnd to master screen --- .../builder/src/components/userInterface/PageLayout.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/components/userInterface/PageLayout.svelte b/packages/builder/src/components/userInterface/PageLayout.svelte index d64ab670ba..b5596f2ae6 100644 --- a/packages/builder/src/components/userInterface/PageLayout.svelte +++ b/packages/builder/src/components/userInterface/PageLayout.svelte @@ -16,12 +16,14 @@ import { pipe } from "components/common/core" import { store } from "builderStore" import { ArrowDownIcon, GridIcon } from "components/common/Icons/" + import { writable } from "svelte/store" export let layout let confirmDeleteDialog let componentToDelete = "" + const dragDropStore = writable({}) const joinPath = join("/") const lastPartOfName = c => @@ -57,7 +59,8 @@ + currentComponent={$store.currentComponentInfo} + {dragDropStore} /> {/if}