forked from tsai/budibase
committed by
GitHub
14 changed files with 388 additions and 304 deletions
@ -0,0 +1,59 @@ |
|||
import { makePropsSafe } from "components/userInterface/pagesParsing/createProps" |
|||
import api from "./api" |
|||
|
|||
export const selectComponent = (state, component) => { |
|||
const componentDef = component._component.startsWith("##") |
|||
? component |
|||
: state.components[component._component] |
|||
state.currentComponentInfo = makePropsSafe(componentDef, component) |
|||
state.currentView = "component" |
|||
return state |
|||
} |
|||
|
|||
export const getParent = (rootProps, child) => { |
|||
let parent |
|||
walkProps(rootProps, (p, breakWalk) => { |
|||
if ( |
|||
p._children && |
|||
(p._children.includes(child) || p._children.some(c => c._id === child)) |
|||
) { |
|||
parent = p |
|||
breakWalk() |
|||
} |
|||
}) |
|||
return parent |
|||
} |
|||
|
|||
export const saveCurrentPreviewItem = s => |
|||
s.currentFrontEndType === "page" |
|||
? savePage(s) |
|||
: saveScreenApi(s.currentPreviewItem, s) |
|||
|
|||
export const savePage = async s => { |
|||
const page = s.pages[s.currentPageName] |
|||
await api.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}`, { |
|||
page: { componentLibraries: s.pages.componentLibraries, ...page }, |
|||
uiFunctions: s.currentPageFunctions, |
|||
screens: page._screens, |
|||
}) |
|||
} |
|||
|
|||
export const saveScreenApi = (screen, s) => { |
|||
api |
|||
.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen) |
|||
.then(() => savePage(s)) |
|||
} |
|||
|
|||
export const walkProps = (props, action, cancelToken = null) => { |
|||
cancelToken = cancelToken || { cancelled: false } |
|||
action(props, () => { |
|||
cancelToken.cancelled = true |
|||
}) |
|||
|
|||
if (props._children) { |
|||
for (let child of props._children) { |
|||
if (cancelToken.cancelled) return |
|||
walkProps(child, action, cancelToken) |
|||
} |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 419 B |
@ -0,0 +1,232 @@ |
|||
<script> |
|||
import { MoreIcon } from "components/common/Icons" |
|||
import { store } from "builderStore" |
|||
import { getComponentDefinition } from "builderStore/store" |
|||
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
|||
import { last, cloneDeep } from "lodash/fp" |
|||
import UIkit from "uikit" |
|||
import { |
|||
selectComponent, |
|||
getParent, |
|||
walkProps, |
|||
saveCurrentPreviewItem, |
|||
} from "builderStore/storeUtils" |
|||
import { uuid } from "builderStore/uuid" |
|||
|
|||
export let component |
|||
|
|||
let confirmDeleteDialog |
|||
let dropdownEl |
|||
|
|||
$: dropdown = UIkit.dropdown(dropdownEl, { |
|||
mode: "click", |
|||
offset: 0, |
|||
pos: "bottom-right", |
|||
"delay-hide": 0, |
|||
animation: false, |
|||
}) |
|||
$: dropdown && UIkit.util.on(dropdown, "shown", () => (hidden = false)) |
|||
$: noChildrenAllowed = |
|||
!component || |
|||
getComponentDefinition($store, component._component).children === false |
|||
$: noPaste = |
|||
!$store.componentToPaste || $store.componentToPaste._id === component._id |
|||
|
|||
const lastPartOfName = c => (c ? last(c._component.split("/")) : "") |
|||
|
|||
const hideDropdown = () => { |
|||
dropdown.hide() |
|||
} |
|||
|
|||
const moveUpComponent = () => { |
|||
store.update(s => { |
|||
const parent = getParent(s.currentPreviewItem.props, component) |
|||
|
|||
if (parent) { |
|||
const currentIndex = parent._children.indexOf(component) |
|||
if (currentIndex === 0) return s |
|||
|
|||
const newChildren = parent._children.filter(c => c !== component) |
|||
newChildren.splice(currentIndex - 1, 0, component) |
|||
parent._children = newChildren |
|||
} |
|||
s.currentComponentInfo = component |
|||
saveCurrentPreviewItem(s) |
|||
|
|||
return s |
|||
}) |
|||
} |
|||
|
|||
const moveDownComponent = () => { |
|||
store.update(s => { |
|||
const parent = getParent(s.currentPreviewItem.props, component) |
|||
|
|||
if (parent) { |
|||
const currentIndex = parent._children.indexOf(component) |
|||
if (currentIndex === parent._children.length - 1) return s |
|||
|
|||
const newChildren = parent._children.filter(c => c !== component) |
|||
newChildren.splice(currentIndex + 1, 0, component) |
|||
parent._children = newChildren |
|||
} |
|||
s.currentComponentInfo = component |
|||
saveCurrentPreviewItem(s) |
|||
|
|||
return s |
|||
}) |
|||
} |
|||
|
|||
const copyComponent = () => { |
|||
store.update(s => { |
|||
const parent = getParent(s.currentPreviewItem.props, component) |
|||
const copiedComponent = cloneDeep(component) |
|||
walkProps(copiedComponent, p => { |
|||
p._id = uuid() |
|||
}) |
|||
parent._children = [...parent._children, copiedComponent] |
|||
saveCurrentPreviewItem(s) |
|||
s.currentComponentInfo = copiedComponent |
|||
return s |
|||
}) |
|||
} |
|||
|
|||
const deleteComponent = () => { |
|||
store.update(state => { |
|||
const parent = getParent(state.currentPreviewItem.props, component) |
|||
|
|||
if (parent) { |
|||
parent._children = parent._children.filter(c => c !== component) |
|||
} |
|||
|
|||
saveCurrentPreviewItem(state) |
|||
|
|||
return state |
|||
}) |
|||
} |
|||
|
|||
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 |
|||
}) |
|||
} |
|||
|
|||
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)) |
|||
|
|||
saveCurrentPreviewItem(s) |
|||
selectComponent(s, componentToPaste) |
|||
|
|||
return s |
|||
}) |
|||
} |
|||
</script> |
|||
|
|||
<div class="root" on:click|stopPropagation={() => {}}> |
|||
<button> |
|||
<MoreIcon /> |
|||
</button> |
|||
<ul class="menu" bind:this={dropdownEl} on:click={hideDropdown}> |
|||
<li on:click={() => confirmDeleteDialog.show()}>Delete</li> |
|||
<li on:click={moveUpComponent}>Move up</li> |
|||
<li on:click={moveDownComponent}>Move down</li> |
|||
<li on:click={copyComponent}>Duplicate</li> |
|||
<li on:click={() => storeComponentForCopy(true)}>Cut</li> |
|||
<li on:click={() => storeComponentForCopy(false)}>Copy</li> |
|||
<hr /> |
|||
<li class:disabled={noPaste} on:click={() => pasteComponent('above')}> |
|||
Paste above |
|||
</li> |
|||
<li class:disabled={noPaste} on:click={() => pasteComponent('below')}> |
|||
Paste below |
|||
</li> |
|||
<li |
|||
class:disabled={noPaste || noChildrenAllowed} |
|||
on:click={() => pasteComponent('inside')}> |
|||
Paste inside |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
|
|||
<ConfirmDialog |
|||
bind:this={confirmDeleteDialog} |
|||
title="Confirm Delete" |
|||
body={`Are you sure you wish to delete this '${lastPartOfName(component)}' component?`} |
|||
okText="Delete Component" |
|||
onOk={deleteComponent} /> |
|||
|
|||
<style> |
|||
.root { |
|||
overflow: hidden; |
|||
z-index: 9; |
|||
} |
|||
|
|||
.root button { |
|||
border-style: none; |
|||
border-radius: 2px; |
|||
padding: 5px; |
|||
background: transparent; |
|||
cursor: pointer; |
|||
color: var(--button-text); |
|||
outline: none; |
|||
} |
|||
|
|||
.menu { |
|||
z-index: 100000; |
|||
overflow: visible; |
|||
padding: 10px 0; |
|||
} |
|||
|
|||
.menu li { |
|||
border-style: none; |
|||
background-color: transparent; |
|||
list-style-type: none; |
|||
padding: 4px 5px 4px 15px; |
|||
margin: 0; |
|||
width: 100%; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.menu li:not(.disabled) { |
|||
cursor: pointer; |
|||
color: var(--ink); |
|||
} |
|||
|
|||
.menu li:not(.disabled):hover { |
|||
color: var(--button-text); |
|||
background-color: var(--grey-light); |
|||
} |
|||
|
|||
.disabled { |
|||
color: var(--grey-dark); |
|||
cursor: default; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue