Merge pull request #8267 from Budibase/dnd-improvements
Drag and drop V2
This commit is contained in:
commit
64b39a8478
|
@ -451,7 +451,7 @@ export const getFrontendStore = () => {
|
|||
...extras,
|
||||
}
|
||||
},
|
||||
create: async (componentName, presetProps) => {
|
||||
create: async (componentName, presetProps, parent, index) => {
|
||||
const state = get(store)
|
||||
const componentInstance = store.actions.components.createInstance(
|
||||
componentName,
|
||||
|
@ -461,48 +461,62 @@ export const getFrontendStore = () => {
|
|||
return
|
||||
}
|
||||
|
||||
// Patch selected screen
|
||||
await store.actions.screens.patch(screen => {
|
||||
// Find the selected component
|
||||
const currentComponent = findComponent(
|
||||
screen.props,
|
||||
state.selectedComponentId
|
||||
)
|
||||
if (!currentComponent) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Find parent node to attach this component to
|
||||
let parentComponent
|
||||
if (currentComponent) {
|
||||
// Use selected component as parent if one is selected
|
||||
const definition = store.actions.components.getDefinition(
|
||||
currentComponent._component
|
||||
)
|
||||
if (definition?.hasChildren) {
|
||||
// Use selected component if it allows children
|
||||
parentComponent = currentComponent
|
||||
// Insert in position if specified
|
||||
if (parent && index != null) {
|
||||
await store.actions.screens.patch(screen => {
|
||||
let parentComponent = findComponent(screen.props, parent)
|
||||
if (!parentComponent._children?.length) {
|
||||
parentComponent._children = [componentInstance]
|
||||
} else {
|
||||
// Otherwise we need to use the parent of this component
|
||||
parentComponent = findComponentParent(
|
||||
screen.props,
|
||||
currentComponent._id
|
||||
)
|
||||
parentComponent._children.splice(index, 0, componentInstance)
|
||||
}
|
||||
} else {
|
||||
// Use screen or layout if no component is selected
|
||||
parentComponent = screen.props
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Attach new component
|
||||
if (!parentComponent) {
|
||||
return false
|
||||
}
|
||||
if (!parentComponent._children) {
|
||||
parentComponent._children = []
|
||||
}
|
||||
parentComponent._children.push(componentInstance)
|
||||
})
|
||||
// Otherwise we work out where this component should be inserted
|
||||
else {
|
||||
await store.actions.screens.patch(screen => {
|
||||
// Find the selected component
|
||||
const currentComponent = findComponent(
|
||||
screen.props,
|
||||
state.selectedComponentId
|
||||
)
|
||||
if (!currentComponent) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Find parent node to attach this component to
|
||||
let parentComponent
|
||||
if (currentComponent) {
|
||||
// Use selected component as parent if one is selected
|
||||
const definition = store.actions.components.getDefinition(
|
||||
currentComponent._component
|
||||
)
|
||||
if (definition?.hasChildren) {
|
||||
// Use selected component if it allows children
|
||||
parentComponent = currentComponent
|
||||
} else {
|
||||
// Otherwise we need to use the parent of this component
|
||||
parentComponent = findComponentParent(
|
||||
screen.props,
|
||||
currentComponent._id
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Use screen or layout if no component is selected
|
||||
parentComponent = screen.props
|
||||
}
|
||||
|
||||
// Attach new component
|
||||
if (!parentComponent) {
|
||||
return false
|
||||
}
|
||||
if (!parentComponent._children) {
|
||||
parentComponent._children = []
|
||||
}
|
||||
parentComponent._children.push(componentInstance)
|
||||
})
|
||||
}
|
||||
|
||||
// Select new component
|
||||
store.update(state => {
|
||||
|
@ -990,6 +1004,19 @@ export const getFrontendStore = () => {
|
|||
}))
|
||||
},
|
||||
},
|
||||
dnd: {
|
||||
start: component => {
|
||||
store.actions.preview.sendEvent("dragging-new-component", {
|
||||
dragging: true,
|
||||
component,
|
||||
})
|
||||
},
|
||||
stop: () => {
|
||||
store.actions.preview.sendEvent("dragging-new-component", {
|
||||
dragging: false,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return store
|
||||
|
|
|
@ -10,6 +10,7 @@ export const syncURLToState = options => {
|
|||
fallbackUrl,
|
||||
store,
|
||||
routify,
|
||||
beforeNavigate,
|
||||
} = options || {}
|
||||
if (
|
||||
!urlParam ||
|
||||
|
@ -41,6 +42,15 @@ export const syncURLToState = options => {
|
|||
|
||||
// Navigate to a certain URL
|
||||
const gotoUrl = (url, params) => {
|
||||
if (beforeNavigate) {
|
||||
const res = beforeNavigate(url, params)
|
||||
if (res?.url) {
|
||||
url = res.url
|
||||
}
|
||||
if (res?.params) {
|
||||
params = res.params
|
||||
}
|
||||
}
|
||||
log("Navigating to", url, "with params", params)
|
||||
cachedGoto(url, params)
|
||||
}
|
||||
|
|
|
@ -213,6 +213,9 @@
|
|||
await store.actions.components.handleEjectBlock(id, definition)
|
||||
} else if (type === "reload-plugin") {
|
||||
await store.actions.components.refreshDefinitions()
|
||||
} else if (type === "drop-new-component") {
|
||||
const { component, parent, index } = data
|
||||
await store.actions.components.create(component, null, parent, index)
|
||||
} else {
|
||||
console.warn(`Client sent unknown event type: ${type}`)
|
||||
}
|
||||
|
|
|
@ -76,6 +76,9 @@
|
|||
const compDef = store.actions.components.getDefinition(
|
||||
$dndStore.source?._component
|
||||
)
|
||||
if (!compDef) {
|
||||
return
|
||||
}
|
||||
const compTypeName = compDef.name.toLowerCase()
|
||||
const path = findComponentPath(currentScreen.props, component._id)
|
||||
|
||||
|
|
|
@ -7,6 +7,18 @@
|
|||
import ComponentListPanel from "./_components/navigation/ComponentListPanel.svelte"
|
||||
import ComponentSettingsPanel from "./_components/settings/ComponentSettingsPanel.svelte"
|
||||
|
||||
const cleanUrl = url => {
|
||||
// Strip trailing slashes
|
||||
if (url?.endsWith("/index")) {
|
||||
url = url.replace("/index", "")
|
||||
}
|
||||
// Hide new component panel whenever component ID changes
|
||||
if (url?.endsWith("/new")) {
|
||||
url = url.replace("/new", "")
|
||||
}
|
||||
return { url }
|
||||
}
|
||||
|
||||
// Keep URL and state in sync for selected component ID
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "componentId",
|
||||
|
@ -15,6 +27,7 @@
|
|||
fallbackUrl: "../",
|
||||
store,
|
||||
routify,
|
||||
beforeNavigate: cleanUrl,
|
||||
})
|
||||
|
||||
onDestroy(stopSyncing)
|
||||
|
|
|
@ -169,6 +169,14 @@
|
|||
window.removeEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
})
|
||||
|
||||
const onDragStart = component => {
|
||||
store.actions.dnd.start(component)
|
||||
}
|
||||
|
||||
const onDragEnd = () => {
|
||||
store.actions.dnd.stop()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container" transition:fly|local={{ x: 260, duration: 300 }}>
|
||||
|
@ -206,6 +214,9 @@
|
|||
<div class="category-label">{category.name}</div>
|
||||
{#each category.children as component}
|
||||
<div
|
||||
draggable="true"
|
||||
on:dragstart={() => onDragStart(component.component)}
|
||||
on:dragend={onDragEnd}
|
||||
data-cy={`component-${component.name}`}
|
||||
class="component"
|
||||
class:selected={selectedIndex ===
|
||||
|
@ -229,8 +240,11 @@
|
|||
<Layout noPadding gap="XS">
|
||||
{#each blocks as block}
|
||||
<div
|
||||
draggable="true"
|
||||
class="component"
|
||||
on:click={() => addComponent(block.component)}
|
||||
on:dragstart={() => onDragStart(block.component)}
|
||||
on:dragend={onDragEnd}
|
||||
>
|
||||
<Icon name={block.icon} />
|
||||
<Body size="XS">{block.name}</Body>
|
||||
|
|
|
@ -85,6 +85,10 @@
|
|||
"icon": "Selection",
|
||||
"hasChildren": true,
|
||||
"showSettingsBar": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 100
|
||||
},
|
||||
"styles": [
|
||||
"padding",
|
||||
"size",
|
||||
|
@ -255,6 +259,10 @@
|
|||
"section"
|
||||
],
|
||||
"showEmptyState": false,
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 100
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "section",
|
||||
|
@ -276,6 +284,10 @@
|
|||
"icon": "Button",
|
||||
"editable": true,
|
||||
"showSettingsBar": true,
|
||||
"size": {
|
||||
"width": 105,
|
||||
"height": 35
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -368,6 +380,10 @@
|
|||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 10
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "select",
|
||||
|
@ -405,6 +421,10 @@
|
|||
],
|
||||
"hasChildren": true,
|
||||
"showSettingsBar": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 100
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataProvider",
|
||||
|
@ -584,6 +604,7 @@
|
|||
]
|
||||
},
|
||||
"card": {
|
||||
"deprecated": true,
|
||||
"name": "Vertical Card",
|
||||
"description": "A basic card component that can contain content and actions.",
|
||||
"icon": "ViewColumn",
|
||||
|
@ -664,6 +685,10 @@
|
|||
],
|
||||
"showSettingsBar": true,
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 30
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -786,6 +811,10 @@
|
|||
],
|
||||
"showSettingsBar": true,
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 40
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -903,6 +932,10 @@
|
|||
"name": "Tag",
|
||||
"icon": "Label",
|
||||
"showSettingsBar": true,
|
||||
"size": {
|
||||
"width": 100,
|
||||
"height": 25
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -954,12 +987,13 @@
|
|||
"name": "Image",
|
||||
"description": "A basic component for displaying images",
|
||||
"icon": "Image",
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"styles": [
|
||||
"size"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 300
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -976,9 +1010,10 @@
|
|||
"styles": [
|
||||
"size"
|
||||
],
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 300
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -1036,9 +1071,10 @@
|
|||
"name": "Icon",
|
||||
"description": "A basic component for displaying icons",
|
||||
"icon": "Shapes",
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 25,
|
||||
"height": 25
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "icon",
|
||||
|
@ -1155,9 +1191,10 @@
|
|||
"icon": "Link",
|
||||
"showSettingsBar": true,
|
||||
"editable": true,
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 200,
|
||||
"height": 30
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -1267,12 +1304,10 @@
|
|||
]
|
||||
},
|
||||
"cardhorizontal": {
|
||||
"deprecated": true,
|
||||
"name": "Horizontal Card",
|
||||
"description": "A basic card component that can contain content and actions.",
|
||||
"icon": "ViewRow",
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -1363,27 +1398,31 @@
|
|||
"name": "Stat Card",
|
||||
"description": "A card component for displaying numbers.",
|
||||
"icon": "Card",
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 260,
|
||||
"height": 143
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Title",
|
||||
"key": "title",
|
||||
"placeholder": "Total Revenue"
|
||||
"placeholder": "Total Revenue",
|
||||
"defaultValue": "Title"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"placeholder": "$1,981,983"
|
||||
"placeholder": "$1,981,983",
|
||||
"defaultValue": "Value"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Label",
|
||||
"key": "label",
|
||||
"placeholder": "Stripe"
|
||||
"placeholder": "Stripe",
|
||||
"defaultValue": "Label"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1391,12 +1430,13 @@
|
|||
"name": "Embed",
|
||||
"icon": "Code",
|
||||
"description": "Embed content from 3rd party sources",
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"styles": [
|
||||
"size"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 100
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -1410,9 +1450,10 @@
|
|||
"name": "Bar Chart",
|
||||
"description": "Bar chart",
|
||||
"icon": "GraphBarVertical",
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -1571,9 +1612,10 @@
|
|||
"name": "Line Chart",
|
||||
"description": "Line chart",
|
||||
"icon": "GraphTrend",
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -1731,9 +1773,10 @@
|
|||
"name": "Area Chart",
|
||||
"description": "Line chart",
|
||||
"icon": "GraphAreaStacked",
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -1903,9 +1946,10 @@
|
|||
"name": "Pie Chart",
|
||||
"description": "Pie chart",
|
||||
"icon": "GraphPie",
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -2031,9 +2075,10 @@
|
|||
"name": "Donut Chart",
|
||||
"description": "Donut chart",
|
||||
"icon": "GraphDonut",
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -2159,9 +2204,10 @@
|
|||
"name": "Candlestick Chart",
|
||||
"description": "Candlestick chart",
|
||||
"icon": "GraphBarVerticalStacked",
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -2266,6 +2312,10 @@
|
|||
"styles": [
|
||||
"size"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 400
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "select",
|
||||
|
@ -2352,6 +2402,10 @@
|
|||
"styles": [
|
||||
"size"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 400
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "number",
|
||||
|
@ -2372,6 +2426,10 @@
|
|||
"size"
|
||||
],
|
||||
"hasChildren": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 400
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "select",
|
||||
|
@ -2398,13 +2456,14 @@
|
|||
"stringfield": {
|
||||
"name": "Text Field",
|
||||
"icon": "Text",
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"styles": [
|
||||
"size"
|
||||
],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 50
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/string",
|
||||
|
@ -2492,9 +2551,10 @@
|
|||
"size"
|
||||
],
|
||||
"editable": true,
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 50
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/number",
|
||||
|
@ -2548,9 +2608,10 @@
|
|||
"size"
|
||||
],
|
||||
"editable": true,
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 50
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/string",
|
||||
|
@ -2604,9 +2665,10 @@
|
|||
"size"
|
||||
],
|
||||
"editable": true,
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 50
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/options",
|
||||
|
@ -2771,9 +2833,10 @@
|
|||
"size"
|
||||
],
|
||||
"editable": true,
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 50
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/array",
|
||||
|
@ -2929,9 +2992,10 @@
|
|||
"name": "Checkbox",
|
||||
"icon": "SelectBox",
|
||||
"editable": true,
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 50
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/boolean",
|
||||
|
@ -3009,6 +3073,10 @@
|
|||
"size"
|
||||
],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 150
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/longform",
|
||||
|
@ -3084,9 +3152,10 @@
|
|||
"size"
|
||||
],
|
||||
"editable": true,
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 50
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/datetime",
|
||||
|
@ -3163,9 +3232,10 @@
|
|||
"styles": [
|
||||
"size"
|
||||
],
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 50
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/barcode/qr",
|
||||
|
@ -3214,29 +3284,27 @@
|
|||
"size"
|
||||
],
|
||||
"draggable": false,
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 320
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataProvider",
|
||||
"label": "Provider",
|
||||
"key": "dataProvider",
|
||||
"required": true
|
||||
"key": "dataProvider"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Latitude Key",
|
||||
"key": "latitudeKey",
|
||||
"dependsOn": "dataProvider",
|
||||
"required": true
|
||||
"dependsOn": "dataProvider"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Longitude Key",
|
||||
"key": "longitudeKey",
|
||||
"dependsOn": "dataProvider",
|
||||
"required": true
|
||||
"dependsOn": "dataProvider"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
|
@ -3330,9 +3398,10 @@
|
|||
"size"
|
||||
],
|
||||
"editable": true,
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 200
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/attachment",
|
||||
|
@ -3387,9 +3456,10 @@
|
|||
"size"
|
||||
],
|
||||
"editable": true,
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 50
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/link",
|
||||
|
@ -3449,6 +3519,10 @@
|
|||
"size"
|
||||
],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 100
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/json",
|
||||
|
@ -3497,6 +3571,10 @@
|
|||
"size"
|
||||
],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 200
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/attachment",
|
||||
|
@ -3559,6 +3637,10 @@
|
|||
"actions": [
|
||||
"RefreshDatasource"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 100
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataSource",
|
||||
|
@ -3639,6 +3721,10 @@
|
|||
],
|
||||
"hasChildren": true,
|
||||
"showEmptyState": false,
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataProvider",
|
||||
|
@ -3737,6 +3823,10 @@
|
|||
"size"
|
||||
],
|
||||
"hasChildren": false,
|
||||
"size": {
|
||||
"width": 200,
|
||||
"height": 50
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataProvider",
|
||||
|
@ -3773,21 +3863,28 @@
|
|||
"styles": [
|
||||
"size"
|
||||
],
|
||||
"size": {
|
||||
"width": 300,
|
||||
"height": 120
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "title",
|
||||
"label": "Title"
|
||||
"label": "Title",
|
||||
"defaultValue": "Title"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "subtitle",
|
||||
"label": "Subtitle"
|
||||
"label": "Subtitle",
|
||||
"defaultValue": "Subtitle"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "description",
|
||||
"label": "Description"
|
||||
"label": "Description",
|
||||
"defaultValue": "Description"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -3831,6 +3928,10 @@
|
|||
"name": "Dynamic Filter",
|
||||
"icon": "Filter",
|
||||
"showSettingsBar": true,
|
||||
"size": {
|
||||
"width": 100,
|
||||
"height": 35
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataProvider",
|
||||
|
@ -3878,6 +3979,10 @@
|
|||
"styles": [
|
||||
"size"
|
||||
],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -4043,6 +4148,10 @@
|
|||
"styles": [
|
||||
"size"
|
||||
],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -4101,19 +4210,22 @@
|
|||
"type": "text",
|
||||
"key": "cardTitle",
|
||||
"label": "Title",
|
||||
"nested": true
|
||||
"nested": true,
|
||||
"defaultValue": "Title"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "cardSubtitle",
|
||||
"label": "Subtitle",
|
||||
"nested": true
|
||||
"nested": true,
|
||||
"defaultValue": "Subtitle"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "cardDescription",
|
||||
"label": "Description",
|
||||
"nested": true
|
||||
"nested": true,
|
||||
"defaultValue": "Description"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -4215,6 +4327,10 @@
|
|||
],
|
||||
"hasChildren": true,
|
||||
"showSettingsBar": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 100
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataSource",
|
||||
|
@ -4437,6 +4553,10 @@
|
|||
"styles": [
|
||||
"size"
|
||||
],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 100
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -4454,6 +4574,10 @@
|
|||
],
|
||||
"block": true,
|
||||
"info": "Form blocks are only compatible with internal or SQL tables",
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 400
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "select",
|
||||
|
|
|
@ -67,6 +67,9 @@
|
|||
// any depth
|
||||
id: $component.id,
|
||||
|
||||
// Name can be used down the tree in placeholders
|
||||
name: $component.name,
|
||||
|
||||
// We register block components with their raw props so that we can eject
|
||||
// blocks later on
|
||||
registerComponent: registerBlockComponent,
|
||||
|
|
|
@ -16,7 +16,14 @@
|
|||
propsAreSame,
|
||||
getSettingsDefinition,
|
||||
} from "utils/componentProps"
|
||||
import { builderStore, devToolsStore, componentStore, appStore } from "stores"
|
||||
import {
|
||||
builderStore,
|
||||
devToolsStore,
|
||||
componentStore,
|
||||
appStore,
|
||||
dndIsDragging,
|
||||
dndComponentPath,
|
||||
} from "stores"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { getActiveConditions, reduceConditionActions } from "utils/conditions"
|
||||
import Placeholder from "components/app/Placeholder.svelte"
|
||||
|
@ -27,6 +34,7 @@
|
|||
export let isLayout = false
|
||||
export let isScreen = false
|
||||
export let isBlock = false
|
||||
export let parent = null
|
||||
|
||||
// Get parent contexts
|
||||
const context = getContext("context")
|
||||
|
@ -97,6 +105,7 @@
|
|||
$builderStore.inBuilder && $builderStore.selectedComponentId === id
|
||||
$: inSelectedPath = $componentStore.selectedComponentPath?.includes(id)
|
||||
$: inDragPath = inSelectedPath && $builderStore.editMode
|
||||
$: inDndPath = $dndComponentPath?.includes(id)
|
||||
|
||||
// Derive definition properties which can all be optional, so need to be
|
||||
// coerced to booleans
|
||||
|
@ -108,7 +117,7 @@
|
|||
// Interactive components can be selected, dragged and highlighted inside
|
||||
// the builder preview
|
||||
$: builderInteractive =
|
||||
$builderStore.inBuilder && insideScreenslot && !isBlock
|
||||
$builderStore.inBuilder && insideScreenslot && !isBlock && !instance.static
|
||||
$: devToolsInteractive = $devToolsStore.allowSelection && !isBlock
|
||||
$: interactive = builderInteractive || devToolsInteractive
|
||||
$: editing = editable && selected && $builderStore.editMode
|
||||
|
@ -118,7 +127,7 @@
|
|||
!isLayout &&
|
||||
!isScreen &&
|
||||
definition?.draggable !== false
|
||||
$: droppable = interactive && !isLayout && !isScreen
|
||||
$: droppable = interactive
|
||||
$: builderHidden =
|
||||
$builderStore.inBuilder && $builderStore.hiddenComponentIds?.includes(id)
|
||||
|
||||
|
@ -126,8 +135,9 @@
|
|||
// Empty states can be shown for these components, but can be disabled
|
||||
// in the component manifest.
|
||||
$: empty =
|
||||
(interactive && !children.length && hasChildren) ||
|
||||
hasMissingRequiredSettings
|
||||
!isBlock &&
|
||||
((interactive && !children.length && hasChildren) ||
|
||||
hasMissingRequiredSettings)
|
||||
$: emptyState = empty && showEmptyState
|
||||
|
||||
// Enrich component settings
|
||||
|
@ -149,6 +159,12 @@
|
|||
// Scroll the selected element into view
|
||||
$: selected && scrollIntoView()
|
||||
|
||||
// When dragging and dropping, pad components to allow dropping between
|
||||
// nested layers. Only reset this when dragging stops.
|
||||
let pad = false
|
||||
$: pad = pad || (interactive && hasChildren && inDndPath)
|
||||
$: $dndIsDragging, (pad = false)
|
||||
|
||||
// Update component context
|
||||
$: store.set({
|
||||
id,
|
||||
|
@ -405,6 +421,11 @@
|
|||
}
|
||||
|
||||
const scrollIntoView = () => {
|
||||
// Don't scroll into view if we selected this component because we were
|
||||
// starting dragging on it
|
||||
if (get(dndIsDragging)) {
|
||||
return
|
||||
}
|
||||
const node = document.getElementsByClassName(id)?.[0]?.children[0]
|
||||
if (!node) {
|
||||
return
|
||||
|
@ -452,17 +473,20 @@
|
|||
class:empty
|
||||
class:interactive
|
||||
class:editing
|
||||
class:pad
|
||||
class:parent={hasChildren}
|
||||
class:block={isBlock}
|
||||
data-id={id}
|
||||
data-name={name}
|
||||
data-icon={icon}
|
||||
data-parent={parent}
|
||||
>
|
||||
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
||||
{#if hasMissingRequiredSettings}
|
||||
<ComponentPlaceholder />
|
||||
{:else if children.length}
|
||||
{#each children as child (child._id)}
|
||||
<svelte:self instance={child} />
|
||||
<svelte:self instance={child} parent={id} />
|
||||
{/each}
|
||||
{:else if emptyState}
|
||||
{#if isScreen}
|
||||
|
@ -481,16 +505,14 @@
|
|||
.component {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.interactive :global(*:hover) {
|
||||
cursor: pointer;
|
||||
.component.pad :global(> *) {
|
||||
padding: var(--spacing-l) !important;
|
||||
gap: var(--spacing-l) !important;
|
||||
border: 2px dashed var(--spectrum-global-color-gray-400) !important;
|
||||
border-radius: 4px !important;
|
||||
transition: padding 260ms ease-out, border 260ms ease-out;
|
||||
}
|
||||
|
||||
.draggable :global(*:hover) {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.editing :global(*:hover) {
|
||||
cursor: auto;
|
||||
.interactive :global(*) {
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,13 +3,14 @@
|
|||
|
||||
const { builderStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
const block = getContext("block")
|
||||
|
||||
export let text
|
||||
</script>
|
||||
|
||||
{#if $builderStore.inBuilder}
|
||||
<div>
|
||||
{text || $component.name || "Placeholder"}
|
||||
{text || block?.name || $component.name || "Placeholder"}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -1,201 +1,298 @@
|
|||
<script context="module">
|
||||
export const Sides = {
|
||||
Top: "Top",
|
||||
Right: "Right",
|
||||
Bottom: "Bottom",
|
||||
Left: "Left",
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { onMount, onDestroy } from "svelte"
|
||||
import { get } from "svelte/store"
|
||||
import IndicatorSet from "./IndicatorSet.svelte"
|
||||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||
import { builderStore } from "stores"
|
||||
import {
|
||||
builderStore,
|
||||
screenStore,
|
||||
dndStore,
|
||||
dndParent,
|
||||
dndIsDragging,
|
||||
} from "stores"
|
||||
import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
import { findComponentById } from "utils/components.js"
|
||||
import { DNDPlaceholderID } from "constants"
|
||||
|
||||
let dragInfo
|
||||
let dropInfo
|
||||
const ThrottleRate = 130
|
||||
|
||||
const getEdges = (bounds, mousePoint) => {
|
||||
const { width, height, top, left } = bounds
|
||||
return {
|
||||
[Sides.Top]: [mousePoint[0], top],
|
||||
[Sides.Right]: [left + width, mousePoint[1]],
|
||||
[Sides.Bottom]: [mousePoint[0], top + height],
|
||||
[Sides.Left]: [left, mousePoint[1]],
|
||||
// Cache some dnd store state as local variables as it massively helps
|
||||
// performance. It lets us avoid calling svelte getters on every DOM action.
|
||||
$: source = $dndStore.source
|
||||
$: target = $dndStore.target
|
||||
$: drop = $dndStore.drop
|
||||
|
||||
// Util to get the inner DOM node by a component ID
|
||||
const getDOMNode = id => {
|
||||
const component = document.getElementsByClassName(id)[0]
|
||||
return [...component.children][0]
|
||||
}
|
||||
|
||||
// Util to calculate the variance of a set of data
|
||||
const variance = arr => {
|
||||
const mean = arr.reduce((a, b) => a + b, 0) / arr.length
|
||||
let squareSum = 0
|
||||
arr.forEach(value => {
|
||||
const delta = value - mean
|
||||
squareSum += delta * delta
|
||||
})
|
||||
return squareSum / arr.length
|
||||
}
|
||||
|
||||
// Callback when drag stops (whether dropped or not)
|
||||
const stopDragging = () => {
|
||||
// Reset listener
|
||||
if (source?.id) {
|
||||
const component = document.getElementsByClassName(source?.id)[0]
|
||||
if (component) {
|
||||
component.removeEventListener("dragend", stopDragging)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const calculatePointDelta = (point1, point2) => {
|
||||
const deltaX = Math.abs(point1[0] - point2[0])
|
||||
const deltaY = Math.abs(point1[1] - point2[1])
|
||||
return Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
||||
}
|
||||
|
||||
const getDOMNodeForComponent = component => {
|
||||
const parent = component.closest(".component")
|
||||
const children = Array.from(parent.children)
|
||||
return children[0]
|
||||
// Reset state
|
||||
dndStore.actions.reset()
|
||||
}
|
||||
|
||||
// Callback when initially starting a drag on a draggable component
|
||||
const onDragStart = e => {
|
||||
const parent = e.target.closest(".component")
|
||||
if (!parent?.classList.contains("draggable")) {
|
||||
const component = e.target.closest(".component")
|
||||
if (!component?.classList.contains("draggable")) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update state
|
||||
dragInfo = {
|
||||
target: parent.dataset.id,
|
||||
parent: parent.dataset.parent,
|
||||
}
|
||||
builderStore.actions.selectComponent(dragInfo.target)
|
||||
builderStore.actions.setDragging(true)
|
||||
// Hide drag ghost image
|
||||
e.dataTransfer.setDragImage(new Image(), 0, 0)
|
||||
|
||||
// Highlight being dragged by setting opacity
|
||||
const child = getDOMNodeForComponent(e.target)
|
||||
if (child) {
|
||||
child.style.opacity = "0.5"
|
||||
}
|
||||
// Add event handler to clear all drag state when dragging ends
|
||||
component.addEventListener("dragend", stopDragging)
|
||||
|
||||
// Update state
|
||||
const id = component.dataset.id
|
||||
const parentId = component.dataset.parent
|
||||
const parent = findComponentById(
|
||||
get(screenStore).activeScreen?.props,
|
||||
parentId
|
||||
)
|
||||
const index = parent._children.findIndex(
|
||||
x => x._id === component.dataset.id
|
||||
)
|
||||
dndStore.actions.startDraggingExistingComponent({
|
||||
id,
|
||||
bounds: component.children[0].getBoundingClientRect(),
|
||||
parent: parentId,
|
||||
index,
|
||||
})
|
||||
builderStore.actions.selectComponent(id)
|
||||
|
||||
// Set initial drop info to show placeholder exactly where the dragged
|
||||
// component is.
|
||||
// Execute this asynchronously to prevent bugs caused by updating state in
|
||||
// the same handler as selecting a new component (which causes a client
|
||||
// re-initialisation).
|
||||
setTimeout(() => {
|
||||
dndStore.actions.updateDrop({
|
||||
parent: parentId,
|
||||
index,
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// Callback when drag stops (whether dropped or not)
|
||||
const onDragEnd = e => {
|
||||
// Reset opacity style
|
||||
if (dragInfo) {
|
||||
const child = getDOMNodeForComponent(e.target)
|
||||
if (child) {
|
||||
child.style.opacity = ""
|
||||
}
|
||||
// Core logic for handling drop events and determining where to render the
|
||||
// drop target placeholder
|
||||
const processEvent = (mouseX, mouseY) => {
|
||||
if (!target) {
|
||||
return null
|
||||
}
|
||||
let { id, parent, node, acceptsChildren, empty } = target
|
||||
|
||||
// If we're over something that does not accept children then we go up a
|
||||
// level and consider the mouse position relative to the parent
|
||||
if (!acceptsChildren) {
|
||||
id = parent
|
||||
empty = false
|
||||
node = getDOMNode(parent)
|
||||
}
|
||||
|
||||
// Reset state and styles
|
||||
dragInfo = null
|
||||
dropInfo = null
|
||||
builderStore.actions.setDragging(false)
|
||||
// We're now hovering over something which does accept children.
|
||||
// If it is empty, just go inside it.
|
||||
if (empty) {
|
||||
dndStore.actions.updateDrop({
|
||||
parent: id,
|
||||
index: 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// As the first DOM node in a component may not necessarily contain the
|
||||
// child components, we can find to try the parent of the first child
|
||||
// component and use that as the real parent DOM node
|
||||
const childNode = node.getElementsByClassName("component")[0]
|
||||
if (childNode?.parentNode) {
|
||||
node = childNode.parentNode
|
||||
}
|
||||
|
||||
// Append an ephemeral div to allow us to determine layout if only one
|
||||
// child exists
|
||||
let ephemeralDiv
|
||||
if (node.children.length === 1) {
|
||||
ephemeralDiv = document.createElement("div")
|
||||
ephemeralDiv.dataset.id = DNDPlaceholderID
|
||||
node.appendChild(ephemeralDiv)
|
||||
}
|
||||
|
||||
// We're now hovering over something which accepts children and is not
|
||||
// empty, so we need to work out where to inside the placeholder
|
||||
// Calculate the coordinates of various locations on each child.
|
||||
const childCoords = [...(node.children || [])].map(node => {
|
||||
const child = node.children?.[0] || node
|
||||
const bounds = child.getBoundingClientRect()
|
||||
return {
|
||||
placeholder: node.dataset.id === DNDPlaceholderID,
|
||||
centerX: bounds.left + bounds.width / 2,
|
||||
centerY: bounds.top + bounds.height / 2,
|
||||
left: bounds.left,
|
||||
right: bounds.right,
|
||||
top: bounds.top,
|
||||
bottom: bounds.bottom,
|
||||
}
|
||||
})
|
||||
|
||||
// Now that we've calculated the position of the children, we no longer need
|
||||
// the ephemeral div
|
||||
if (ephemeralDiv) {
|
||||
node.removeChild(ephemeralDiv)
|
||||
}
|
||||
|
||||
// Calculate the variance between each set of positions on the children
|
||||
const variances = Object.keys(childCoords[0])
|
||||
.filter(x => x !== "placeholder")
|
||||
.map(key => {
|
||||
const coords = childCoords.map(x => x[key])
|
||||
return {
|
||||
variance: variance(coords),
|
||||
side: key,
|
||||
}
|
||||
})
|
||||
|
||||
// Sort by variance. The lowest variance position indicates whether we are
|
||||
// in a row or column layout
|
||||
variances.sort((a, b) => {
|
||||
return a.variance < b.variance ? -1 : 1
|
||||
})
|
||||
const column = ["centerX", "left", "right"].includes(variances[0].side)
|
||||
|
||||
// Calculate breakpoints between child components so we can determine the
|
||||
// index to drop the component in.
|
||||
// We want to ignore the placeholder from this calculation as it should not
|
||||
// be considered a real child of the parent.
|
||||
let breakpoints = childCoords
|
||||
.filter(x => !x.placeholder)
|
||||
.map(x => {
|
||||
return column ? x.centerY : x.centerX
|
||||
})
|
||||
|
||||
// Determine the index to drop the component in
|
||||
const mousePosition = column ? mouseY : mouseX
|
||||
let idx = 0
|
||||
while (idx < breakpoints.length && breakpoints[idx] < mousePosition) {
|
||||
idx++
|
||||
}
|
||||
dndStore.actions.updateDrop({
|
||||
parent: id,
|
||||
index: idx,
|
||||
})
|
||||
}
|
||||
const throttledProcessEvent = Utils.throttle(processEvent, ThrottleRate)
|
||||
|
||||
const handleEvent = e => {
|
||||
e.preventDefault()
|
||||
throttledProcessEvent(e.clientX, e.clientY)
|
||||
}
|
||||
|
||||
// Callback when on top of a component
|
||||
const onDragOver = e => {
|
||||
// Skip if we aren't validly dragging currently
|
||||
if (!dragInfo || !dropInfo) {
|
||||
if (!source || !target) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
const { droppableInside, bounds } = dropInfo
|
||||
const { top, left, height, width } = bounds
|
||||
const mouseY = e.clientY
|
||||
const mouseX = e.clientX
|
||||
const snapFactor = droppableInside ? 0.33 : 0.5
|
||||
const snapLimitV = Math.min(40, height * snapFactor)
|
||||
const snapLimitH = Math.min(40, width * snapFactor)
|
||||
|
||||
// Determine all sies we are within snap range of
|
||||
let sides = []
|
||||
if (mouseY <= top + snapLimitV) {
|
||||
sides.push(Sides.Top)
|
||||
} else if (mouseY >= top + height - snapLimitV) {
|
||||
sides.push(Sides.Bottom)
|
||||
}
|
||||
if (mouseX < left + snapLimitH) {
|
||||
sides.push(Sides.Left)
|
||||
} else if (mouseX > left + width - snapLimitH) {
|
||||
sides.push(Sides.Right)
|
||||
}
|
||||
|
||||
// When no edges match, drop inside if possible
|
||||
if (!sides.length) {
|
||||
dropInfo.mode = droppableInside ? "inside" : null
|
||||
dropInfo.side = null
|
||||
return
|
||||
}
|
||||
|
||||
// When one edge matches, use that edge
|
||||
if (sides.length === 1) {
|
||||
dropInfo.side = sides[0]
|
||||
if ([Sides.Top, Sides.Left].includes(sides[0])) {
|
||||
dropInfo.mode = "above"
|
||||
} else {
|
||||
dropInfo.mode = "below"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// When 2 edges match, work out which is closer
|
||||
const mousePoint = [mouseX, mouseY]
|
||||
const edges = getEdges(bounds, mousePoint)
|
||||
const edge1 = edges[sides[0]]
|
||||
const delta1 = calculatePointDelta(mousePoint, edge1)
|
||||
const edge2 = edges[sides[1]]
|
||||
const delta2 = calculatePointDelta(mousePoint, edge2)
|
||||
const edge = delta1 < delta2 ? sides[0] : sides[1]
|
||||
dropInfo.side = edge
|
||||
if ([Sides.Top, Sides.Left].includes(edge)) {
|
||||
dropInfo.mode = "above"
|
||||
} else {
|
||||
dropInfo.mode = "below"
|
||||
}
|
||||
handleEvent(e)
|
||||
}
|
||||
|
||||
// Callback when entering a potential drop target
|
||||
const onDragEnter = e => {
|
||||
// Skip if we aren't validly dragging currently
|
||||
if (!dragInfo || !e.target.closest) {
|
||||
if (!source) {
|
||||
return
|
||||
}
|
||||
|
||||
const element = e.target.closest(".component:not(.block)")
|
||||
if (
|
||||
element &&
|
||||
element.classList.contains("droppable") &&
|
||||
element.dataset.id !== dragInfo.target
|
||||
) {
|
||||
// Do nothing if this is the same target
|
||||
if (element.dataset.id === dropInfo?.target) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the dragging flag is always set.
|
||||
// There's a bit of a race condition between the app reinitialisation
|
||||
// after selecting the DND component and setting this the first time
|
||||
if (!get(builderStore).isDragging) {
|
||||
builderStore.actions.setDragging(true)
|
||||
}
|
||||
|
||||
// Store target ID
|
||||
const target = element.dataset.id
|
||||
|
||||
// Precompute and store some info to avoid recalculating everything in
|
||||
// dragOver
|
||||
const child = getDOMNodeForComponent(e.target)
|
||||
const bounds = child.getBoundingClientRect()
|
||||
dropInfo = {
|
||||
target,
|
||||
name: element.dataset.name,
|
||||
icon: element.dataset.icon,
|
||||
droppableInside: element.classList.contains("empty"),
|
||||
bounds,
|
||||
}
|
||||
} else {
|
||||
dropInfo = null
|
||||
// Find the next valid component to consider dropping over, ignoring nested
|
||||
// block components
|
||||
const component = e.target?.closest?.(
|
||||
`.component:not(.block):not(.${source.id})`
|
||||
)
|
||||
if (component && component.classList.contains("droppable")) {
|
||||
dndStore.actions.updateTarget({
|
||||
id: component.dataset.id,
|
||||
parent: component.dataset.parent,
|
||||
node: getDOMNode(component.dataset.id),
|
||||
empty: component.classList.contains("empty"),
|
||||
acceptsChildren: component.classList.contains("parent"),
|
||||
})
|
||||
handleEvent(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Callback when leaving a potential drop target.
|
||||
// Since we don't style our targets, we don't need to unset anything.
|
||||
const onDragLeave = () => {}
|
||||
|
||||
// Callback when dropping a drag on top of some component
|
||||
const onDrop = e => {
|
||||
e.preventDefault()
|
||||
if (dropInfo?.mode) {
|
||||
const onDrop = () => {
|
||||
if (!source || !drop?.parent || drop?.index == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we're adding a new component rather than moving one
|
||||
if (source.newComponentType) {
|
||||
builderStore.actions.dropNewComponent(
|
||||
source.newComponentType,
|
||||
drop.parent,
|
||||
drop.index
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert parent + index into target + mode
|
||||
let legacyDropTarget, legacyDropMode
|
||||
const parent = findComponentById(
|
||||
get(screenStore).activeScreen?.props,
|
||||
drop.parent
|
||||
)
|
||||
if (!parent) {
|
||||
return
|
||||
}
|
||||
|
||||
// Do nothing if we didn't change the location
|
||||
if (source.parent === drop.parent && source.index === drop.index) {
|
||||
return
|
||||
}
|
||||
|
||||
// Filter out source component and placeholder from consideration
|
||||
const children = parent._children?.filter(
|
||||
x => x._id !== DNDPlaceholderID && x._id !== source.id
|
||||
)
|
||||
|
||||
// Use inside if no existing children
|
||||
if (!children?.length) {
|
||||
legacyDropTarget = parent._id
|
||||
legacyDropMode = "inside"
|
||||
} else if (drop.index === 0) {
|
||||
legacyDropTarget = children[0]?._id
|
||||
legacyDropMode = "above"
|
||||
} else {
|
||||
legacyDropTarget = children[drop.index - 1]?._id
|
||||
legacyDropMode = "below"
|
||||
}
|
||||
|
||||
if (legacyDropTarget && legacyDropMode) {
|
||||
builderStore.actions.moveComponent(
|
||||
dragInfo.target,
|
||||
dropInfo.target,
|
||||
dropInfo.mode
|
||||
source.id,
|
||||
legacyDropTarget,
|
||||
legacyDropMode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -203,39 +300,32 @@
|
|||
onMount(() => {
|
||||
// Events fired on the draggable target
|
||||
document.addEventListener("dragstart", onDragStart, false)
|
||||
document.addEventListener("dragend", onDragEnd, false)
|
||||
|
||||
// Events fired on the drop targets
|
||||
document.addEventListener("dragover", onDragOver, false)
|
||||
document.addEventListener("dragenter", onDragEnter, false)
|
||||
document.addEventListener("dragleave", onDragLeave, false)
|
||||
document.addEventListener("drop", onDrop, false)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
// Events fired on the draggable target
|
||||
document.removeEventListener("dragstart", onDragStart, false)
|
||||
document.removeEventListener("dragend", onDragEnd, false)
|
||||
|
||||
// Events fired on the drop targets
|
||||
document.removeEventListener("dragover", onDragOver, false)
|
||||
document.removeEventListener("dragenter", onDragEnter, false)
|
||||
document.removeEventListener("dragleave", onDragLeave, false)
|
||||
document.removeEventListener("drop", onDrop, false)
|
||||
})
|
||||
</script>
|
||||
|
||||
<IndicatorSet
|
||||
componentId={dropInfo?.mode === "inside" ? dropInfo.target : null}
|
||||
componentId={$dndParent}
|
||||
color="var(--spectrum-global-color-static-green-500)"
|
||||
zIndex="930"
|
||||
transition
|
||||
prefix="Inside"
|
||||
/>
|
||||
|
||||
<DNDPositionIndicator
|
||||
{dropInfo}
|
||||
color="var(--spectrum-global-color-static-green-500)"
|
||||
zIndex="940"
|
||||
transition
|
||||
/>
|
||||
{#if $dndIsDragging}
|
||||
<DNDPlaceholderOverlay />
|
||||
{/if}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<script>
|
||||
import { dndBounds } from "stores"
|
||||
import { DNDPlaceholderID } from "constants"
|
||||
|
||||
$: style = getStyle($dndBounds)
|
||||
|
||||
const getStyle = bounds => {
|
||||
if (!bounds) {
|
||||
return null
|
||||
}
|
||||
return `--height: ${bounds.height}px; --width: ${bounds.width}px;`
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if style}
|
||||
<div class="wrapper">
|
||||
<div class="placeholder" id={DNDPlaceholderID} {style} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
.placeholder {
|
||||
display: block;
|
||||
height: var(--height);
|
||||
width: var(--width);
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { DNDPlaceholderID } from "constants"
|
||||
import { domDebounce } from "utils/domDebounce.js"
|
||||
|
||||
let left, top, height, width
|
||||
|
||||
const updatePosition = () => {
|
||||
const node = document.getElementById(DNDPlaceholderID)
|
||||
if (!node) {
|
||||
height = 0
|
||||
width = 0
|
||||
} else {
|
||||
const bounds = node.getBoundingClientRect()
|
||||
left = bounds.left
|
||||
top = bounds.top
|
||||
height = bounds.height
|
||||
width = bounds.width
|
||||
}
|
||||
}
|
||||
const debouncedUpdate = domDebounce(updatePosition)
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(debouncedUpdate, 100)
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if left != null && top != null && width && height}
|
||||
<div
|
||||
class="overlay"
|
||||
style="left: {left}px; top: {top}px; width: {width}px; height: {height}px;"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.overlay {
|
||||
position: fixed;
|
||||
z-index: 800;
|
||||
background: hsl(160, 64%, 90%);
|
||||
border-radius: 4px;
|
||||
transition: all 130ms ease-out;
|
||||
border: 2px solid var(--spectrum-global-color-static-green-500);
|
||||
}
|
||||
</style>
|
|
@ -1,66 +0,0 @@
|
|||
<script>
|
||||
import Indicator from "./Indicator.svelte"
|
||||
import { Sides } from "./DNDHandler.svelte"
|
||||
|
||||
export let dropInfo
|
||||
export let zIndex
|
||||
export let color
|
||||
export let transition
|
||||
|
||||
$: dimensions = getDimensions(dropInfo)
|
||||
$: prefix = dropInfo?.mode === "above" ? "Before" : "After"
|
||||
$: text = `${prefix} ${dropInfo?.name}`
|
||||
$: icon = dropInfo?.icon
|
||||
$: renderKey = `${dropInfo?.target}-${dropInfo?.side}`
|
||||
|
||||
const getDimensions = info => {
|
||||
const { bounds, side } = info ?? {}
|
||||
if (!bounds || !side) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get preview offset
|
||||
const root = document.getElementById("clip-root")
|
||||
const rootBounds = root.getBoundingClientRect()
|
||||
|
||||
// Subtract preview offset from bounds
|
||||
let { left, top, width, height } = bounds
|
||||
left -= rootBounds.left
|
||||
top -= rootBounds.top
|
||||
|
||||
// Determine position
|
||||
if (side === Sides.Top || side === Sides.Bottom) {
|
||||
return {
|
||||
top: side === Sides.Top ? top - 4 : top + height,
|
||||
left: left - 2,
|
||||
width: width + 4,
|
||||
height: 0,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
top: top - 2,
|
||||
left: side === Sides.Left ? left - 4 : left + width,
|
||||
width: 0,
|
||||
height: height + 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#key renderKey}
|
||||
{#if dimensions && dropInfo?.mode !== "inside"}
|
||||
<Indicator
|
||||
left={Math.round(dimensions.left)}
|
||||
top={Math.round(dimensions.top)}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
{text}
|
||||
{icon}
|
||||
{zIndex}
|
||||
{color}
|
||||
{transition}
|
||||
alignRight={dropInfo?.side === Sides.Right}
|
||||
line
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { onMount, onDestroy } from "svelte"
|
||||
import IndicatorSet from "./IndicatorSet.svelte"
|
||||
import { builderStore } from "stores"
|
||||
import { builderStore, dndIsDragging } from "stores"
|
||||
|
||||
let componentId
|
||||
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
||||
|
@ -30,7 +30,7 @@
|
|||
</script>
|
||||
|
||||
<IndicatorSet
|
||||
componentId={$builderStore.isDragging ? null : componentId}
|
||||
componentId={$dndIsDragging ? null : componentId}
|
||||
color="var(--spectrum-global-color-static-blue-200)"
|
||||
transition
|
||||
{zIndex}
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
|
||||
<div
|
||||
in:fade={{
|
||||
delay: transition ? 130 : 0,
|
||||
duration: transition ? 130 : 0,
|
||||
delay: transition ? 100 : 0,
|
||||
duration: transition ? 100 : 0,
|
||||
}}
|
||||
class="indicator"
|
||||
class:flipped
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { builderStore } from "stores"
|
||||
import { builderStore, dndIsDragging } from "stores"
|
||||
import IndicatorSet from "./IndicatorSet.svelte"
|
||||
|
||||
$: color = $builderStore.editMode
|
||||
|
@ -8,7 +8,7 @@
|
|||
</script>
|
||||
|
||||
<IndicatorSet
|
||||
componentId={$builderStore.selectedComponentId}
|
||||
componentId={$dndIsDragging ? null : $builderStore.selectedComponentId}
|
||||
{color}
|
||||
zIndex="910"
|
||||
transition
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import SettingsButton from "./SettingsButton.svelte"
|
||||
import SettingsColorPicker from "./SettingsColorPicker.svelte"
|
||||
import SettingsPicker from "./SettingsPicker.svelte"
|
||||
import { builderStore, componentStore } from "stores"
|
||||
import { builderStore, componentStore, dndIsDragging } from "stores"
|
||||
import { domDebounce } from "utils/domDebounce"
|
||||
|
||||
const verticalOffset = 36
|
||||
|
@ -16,7 +16,7 @@
|
|||
let measured = false
|
||||
|
||||
$: definition = $componentStore.selectedComponentDefinition
|
||||
$: showBar = definition?.showSettingsBar && !$builderStore.isDragging
|
||||
$: showBar = definition?.showSettingsBar && !$dndIsDragging
|
||||
$: settings = getBarSettings(definition)
|
||||
|
||||
const getBarSettings = definition => {
|
||||
|
|
|
@ -30,3 +30,7 @@ export const ActionTypes = {
|
|||
ClearForm: "ClearForm",
|
||||
ChangeFormStep: "ChangeFormStep",
|
||||
}
|
||||
|
||||
export const DNDPlaceholderID = "dnd-placeholder"
|
||||
export const DNDPlaceholderType = "dnd-placeholder"
|
||||
export const ScreenslotType = "screenslot"
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
blockStore,
|
||||
componentStore,
|
||||
environmentStore,
|
||||
dndStore,
|
||||
} from "./stores"
|
||||
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
|
||||
import { get } from "svelte/store"
|
||||
|
@ -25,6 +26,7 @@ let app
|
|||
const loadBudibase = async () => {
|
||||
// Update builder store with any builder flags
|
||||
builderStore.set({
|
||||
...get(builderStore),
|
||||
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
|
||||
layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
|
||||
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
|
||||
|
@ -59,6 +61,15 @@ const loadBudibase = async () => {
|
|||
if (name === "eject-block") {
|
||||
const block = blockStore.actions.getBlock(payload)
|
||||
block?.eject()
|
||||
} else if (name === "dragging-new-component") {
|
||||
const { dragging, component } = payload
|
||||
if (dragging) {
|
||||
const definition =
|
||||
componentStore.actions.getComponentDefinition(component)
|
||||
dndStore.actions.startDraggingNewComponent({ component, definition })
|
||||
} else {
|
||||
dndStore.actions.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ const createBuilderStore = () => {
|
|||
theme: null,
|
||||
customTheme: null,
|
||||
previewDevice: "desktop",
|
||||
isDragging: false,
|
||||
navigation: null,
|
||||
hiddenComponentIds: [],
|
||||
usedPlugins: null,
|
||||
|
@ -67,11 +66,12 @@ const createBuilderStore = () => {
|
|||
mode,
|
||||
})
|
||||
},
|
||||
setDragging: dragging => {
|
||||
if (dragging === get(store).isDragging) {
|
||||
return
|
||||
}
|
||||
store.update(state => ({ ...state, isDragging: dragging }))
|
||||
dropNewComponent: (component, parent, index) => {
|
||||
dispatchEvent("drop-new-component", {
|
||||
component,
|
||||
parent,
|
||||
index,
|
||||
})
|
||||
},
|
||||
setEditMode: enabled => {
|
||||
if (enabled === get(store).editMode) {
|
||||
|
|
|
@ -5,7 +5,9 @@ import { devToolsStore } from "./devTools"
|
|||
import { screenStore } from "./screens"
|
||||
import { builderStore } from "./builder"
|
||||
import Router from "../components/Router.svelte"
|
||||
import DNDPlaceholder from "../components/preview/DNDPlaceholder.svelte"
|
||||
import * as AppComponents from "../components/app/index.js"
|
||||
import { DNDPlaceholderType, ScreenslotType } from "../constants.js"
|
||||
|
||||
const budibasePrefix = "@budibase/standard-components/"
|
||||
|
||||
|
@ -18,26 +20,21 @@ const createComponentStore = () => {
|
|||
|
||||
const derivedStore = derived(
|
||||
[store, builderStore, devToolsStore, screenStore],
|
||||
([$store, $builderState, $devToolsState, $screenState]) => {
|
||||
([$store, $builderStore, $devToolsStore, $screenStore]) => {
|
||||
const { inBuilder, selectedComponentId } = $builderStore
|
||||
|
||||
// Avoid any of this logic if we aren't in the builder preview
|
||||
if (!$builderState.inBuilder && !$devToolsState.visible) {
|
||||
if (!inBuilder && !$devToolsStore.visible) {
|
||||
return {}
|
||||
}
|
||||
|
||||
// Derive the selected component instance and definition
|
||||
let asset
|
||||
const { screen, selectedComponentId } = $builderState
|
||||
if ($builderState.inBuilder) {
|
||||
asset = screen
|
||||
} else {
|
||||
asset = $screenState.activeScreen
|
||||
}
|
||||
const component = findComponentById(asset?.props, selectedComponentId)
|
||||
const root = $screenStore.activeScreen?.props
|
||||
const component = findComponentById(root, selectedComponentId)
|
||||
const definition = getComponentDefinition(component?._component)
|
||||
|
||||
// Derive the selected component path
|
||||
const path =
|
||||
findComponentPathById(asset?.props, selectedComponentId) || []
|
||||
const selectedPath =
|
||||
findComponentPathById(root, selectedComponentId) || []
|
||||
|
||||
return {
|
||||
customComponentManifest: $store.customComponentManifest,
|
||||
|
@ -45,9 +42,8 @@ const createComponentStore = () => {
|
|||
$store.mountedComponents[selectedComponentId],
|
||||
selectedComponent: component,
|
||||
selectedComponentDefinition: definition,
|
||||
selectedComponentPath: path?.map(component => component._id),
|
||||
selectedComponentPath: selectedPath?.map(component => component._id),
|
||||
mountedComponentCount: Object.keys($store.mountedComponents).length,
|
||||
currentAsset: asset,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -95,8 +91,8 @@ const createComponentStore = () => {
|
|||
}
|
||||
|
||||
const getComponentById = id => {
|
||||
const asset = get(derivedStore).currentAsset
|
||||
return findComponentById(asset?.props, id)
|
||||
const root = get(screenStore).activeScreen?.props
|
||||
return findComponentById(root, id)
|
||||
}
|
||||
|
||||
const getComponentDefinition = type => {
|
||||
|
@ -105,8 +101,10 @@ const createComponentStore = () => {
|
|||
}
|
||||
|
||||
// Screenslot is an edge case
|
||||
if (type === "screenslot") {
|
||||
if (type === ScreenslotType) {
|
||||
type = `${budibasePrefix}${type}`
|
||||
} else if (type === DNDPlaceholderType) {
|
||||
return {}
|
||||
}
|
||||
|
||||
// Handle built-in components
|
||||
|
@ -124,8 +122,10 @@ const createComponentStore = () => {
|
|||
if (!type) {
|
||||
return null
|
||||
}
|
||||
if (type === "screenslot") {
|
||||
if (type === ScreenslotType) {
|
||||
return Router
|
||||
} else if (type === DNDPlaceholderType) {
|
||||
return DNDPlaceholder
|
||||
}
|
||||
|
||||
// Handle budibase components
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { derived } from "svelte/store"
|
||||
import { devToolsStore } from "../devTools.js"
|
||||
import { authStore } from "../auth.js"
|
||||
|
||||
// Derive the current role of the logged-in user
|
||||
export const currentRole = derived(
|
||||
[devToolsStore, authStore],
|
||||
([$devToolsStore, $authStore]) => {
|
||||
return ($devToolsStore.enabled && $devToolsStore.role) || $authStore?.roleId
|
||||
}
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
import { derived } from "svelte/store"
|
||||
import { findComponentPathById } from "utils/components.js"
|
||||
import { dndParent } from "../dnd.js"
|
||||
import { screenStore } from "../screens.js"
|
||||
|
||||
export const dndComponentPath = derived(
|
||||
[dndParent, screenStore],
|
||||
([$dndParent, $screenStore]) => {
|
||||
const root = $screenStore.activeScreen?.props
|
||||
const path = findComponentPathById(root, $dndParent) || []
|
||||
return path?.map(component => component._id)
|
||||
}
|
||||
)
|
|
@ -0,0 +1,5 @@
|
|||
// These derived stores are pulled out from their parent stores to avoid
|
||||
// dependency loops. By inverting store dependencies and extracting them
|
||||
// separately we can keep our actual stores lean and performant.
|
||||
export { currentRole } from "./currentRole.js"
|
||||
export { dndComponentPath } from "./dndComponentPath.js"
|
|
@ -0,0 +1,90 @@
|
|||
import { writable, derived } from "svelte/store"
|
||||
|
||||
const createDndStore = () => {
|
||||
const initialState = {
|
||||
// Info about the dragged component
|
||||
source: null,
|
||||
|
||||
// Info about the target component being hovered over
|
||||
target: null,
|
||||
|
||||
// Info about where the component would be dropped
|
||||
drop: null,
|
||||
}
|
||||
const store = writable(initialState)
|
||||
|
||||
const startDraggingExistingComponent = ({ id, parent, bounds, index }) => {
|
||||
store.set({
|
||||
...initialState,
|
||||
source: { id, parent, bounds, index },
|
||||
})
|
||||
}
|
||||
|
||||
const startDraggingNewComponent = ({ component, definition }) => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get size of new component so we can show a properly sized placeholder
|
||||
const width = definition?.size?.width || 128
|
||||
const height = definition?.size?.height || 64
|
||||
|
||||
store.set({
|
||||
...initialState,
|
||||
source: {
|
||||
id: null,
|
||||
parent: null,
|
||||
bounds: { height, width },
|
||||
index: null,
|
||||
newComponentType: component,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const updateTarget = ({ id, parent, node, empty, acceptsChildren }) => {
|
||||
store.update(state => {
|
||||
state.target = { id, parent, node, empty, acceptsChildren }
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const updateDrop = ({ parent, index }) => {
|
||||
store.update(state => {
|
||||
state.drop = { parent, index }
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
store.set(initialState)
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
actions: {
|
||||
startDraggingExistingComponent,
|
||||
startDraggingNewComponent,
|
||||
updateTarget,
|
||||
updateDrop,
|
||||
reset,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const dndStore = createDndStore()
|
||||
|
||||
// The DND store is updated extremely frequently, so we can greatly improve
|
||||
// performance by deriving any state that needs to be externally observed.
|
||||
// By doing this and using primitives, we can avoid invalidating other stores
|
||||
// or components which depend on DND state unless values actually change.
|
||||
export const dndIsDragging = derived(dndStore, $dndStore => !!$dndStore.source)
|
||||
export const dndParent = derived(dndStore, $dndStore => $dndStore.drop?.parent)
|
||||
export const dndIndex = derived(dndStore, $dndStore => $dndStore.drop?.index)
|
||||
export const dndBounds = derived(
|
||||
dndStore,
|
||||
$dndStore => $dndStore.source?.bounds
|
||||
)
|
||||
export const dndIsNewComponent = derived(
|
||||
dndStore,
|
||||
$dndStore => $dndStore.source?.newComponentType != null
|
||||
)
|
|
@ -1,7 +1,3 @@
|
|||
import { derived } from "svelte/store"
|
||||
import { devToolsStore } from "./devTools.js"
|
||||
import { authStore } from "./auth.js"
|
||||
|
||||
export { authStore } from "./auth"
|
||||
export { appStore } from "./app"
|
||||
export { notificationStore } from "./notification"
|
||||
|
@ -19,6 +15,14 @@ export { uploadStore } from "./uploads.js"
|
|||
export { rowSelectionStore } from "./rowSelection.js"
|
||||
export { blockStore } from "./blocks.js"
|
||||
export { environmentStore } from "./environment"
|
||||
export {
|
||||
dndStore,
|
||||
dndIndex,
|
||||
dndParent,
|
||||
dndBounds,
|
||||
dndIsNewComponent,
|
||||
dndIsDragging,
|
||||
} from "./dnd"
|
||||
|
||||
// Context stores are layered and duplicated, so it is not a singleton
|
||||
export { createContextStore } from "./context"
|
||||
|
@ -26,10 +30,5 @@ export { createContextStore } from "./context"
|
|||
// Initialises an app by loading screens and routes
|
||||
export { initialise } from "./initialise"
|
||||
|
||||
// Derive the current role of the logged-in user
|
||||
export const currentRole = derived(
|
||||
[devToolsStore, authStore],
|
||||
([$devToolsStore, $authStore]) => {
|
||||
return ($devToolsStore.enabled && $devToolsStore.role) || $authStore?.roleId
|
||||
}
|
||||
)
|
||||
// Derived state
|
||||
export * from "./derived"
|
||||
|
|
|
@ -2,18 +2,36 @@ import { derived } from "svelte/store"
|
|||
import { routeStore } from "./routes"
|
||||
import { builderStore } from "./builder"
|
||||
import { appStore } from "./app"
|
||||
import { dndIndex, dndParent, dndIsNewComponent } from "./dnd.js"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { findComponentById, findComponentParent } from "../utils/components.js"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { DNDPlaceholderID, DNDPlaceholderType } from "constants"
|
||||
|
||||
const createScreenStore = () => {
|
||||
const store = derived(
|
||||
[appStore, routeStore, builderStore],
|
||||
([$appStore, $routeStore, $builderStore]) => {
|
||||
[
|
||||
appStore,
|
||||
routeStore,
|
||||
builderStore,
|
||||
dndParent,
|
||||
dndIndex,
|
||||
dndIsNewComponent,
|
||||
],
|
||||
([
|
||||
$appStore,
|
||||
$routeStore,
|
||||
$builderStore,
|
||||
$dndParent,
|
||||
$dndIndex,
|
||||
$dndIsNewComponent,
|
||||
]) => {
|
||||
let activeLayout, activeScreen
|
||||
let screens
|
||||
|
||||
if ($builderStore.inBuilder) {
|
||||
// Use builder defined definitions if inside the builder preview
|
||||
activeScreen = $builderStore.screen
|
||||
activeScreen = Helpers.cloneDeep($builderStore.screen)
|
||||
screens = [activeScreen]
|
||||
|
||||
// Legacy - allow the builder to specify a layout
|
||||
|
@ -24,8 +42,10 @@ const createScreenStore = () => {
|
|||
// Find the correct screen by matching the current route
|
||||
screens = $appStore.screens || []
|
||||
if ($routeStore.activeRoute) {
|
||||
activeScreen = screens.find(
|
||||
screen => screen._id === $routeStore.activeRoute.screenId
|
||||
activeScreen = Helpers.cloneDeep(
|
||||
screens.find(
|
||||
screen => screen._id === $routeStore.activeRoute.screenId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -40,6 +60,37 @@ const createScreenStore = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Insert DND placeholder if required
|
||||
if (activeScreen && $dndParent && $dndIndex != null) {
|
||||
// Remove selected component from tree if we are moving an existing
|
||||
// component
|
||||
const { selectedComponentId } = $builderStore
|
||||
if (!$dndIsNewComponent) {
|
||||
let selectedParent = findComponentParent(
|
||||
activeScreen.props,
|
||||
selectedComponentId
|
||||
)
|
||||
if (selectedParent) {
|
||||
selectedParent._children = selectedParent._children?.filter(
|
||||
x => x._id !== selectedComponentId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert placeholder component
|
||||
const placeholder = {
|
||||
_component: DNDPlaceholderID,
|
||||
_id: DNDPlaceholderType,
|
||||
static: true,
|
||||
}
|
||||
let parent = findComponentById(activeScreen.props, $dndParent)
|
||||
if (!parent._children?.length) {
|
||||
parent._children = [placeholder]
|
||||
} else {
|
||||
parent._children.splice($dndIndex, 0, placeholder)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign ranks to screens, preferring higher roles and home screens
|
||||
screens.forEach(screen => {
|
||||
const roleId = screen.routing.roleId
|
||||
|
|
|
@ -60,3 +60,25 @@ export const findChildrenByType = (component, type, children = []) => {
|
|||
findChildrenByType(child, type, children)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively searches for the parent component of a specific component ID
|
||||
*/
|
||||
export const findComponentParent = (rootComponent, id, parentComponent) => {
|
||||
if (!rootComponent || !id) {
|
||||
return null
|
||||
}
|
||||
if (rootComponent._id === id) {
|
||||
return parentComponent
|
||||
}
|
||||
if (!rootComponent._children) {
|
||||
return null
|
||||
}
|
||||
for (const child of rootComponent._children) {
|
||||
const childResult = findComponentParent(child, id, rootComponent)
|
||||
if (childResult) {
|
||||
return childResult
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ export const styleable = (node, styles = {}) => {
|
|||
const setupStyles = (newStyles = {}) => {
|
||||
let baseStyles = {}
|
||||
if (newStyles.empty) {
|
||||
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-600)"
|
||||
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-400)"
|
||||
baseStyles.padding = "var(--spacing-l)"
|
||||
baseStyles.overflow = "hidden"
|
||||
}
|
||||
|
|
|
@ -40,3 +40,37 @@ export const debounce = (callback, minDelay = 1000) => {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to throttle invocations of a synchronous function. This is better
|
||||
* than a simple debounce invocation for a number of reasons. Features include:
|
||||
* - First invocation is immediate (no initial delay)
|
||||
* - Every invocation has the latest params (no stale params)
|
||||
* - There will always be a final invocation with the last params (no missing
|
||||
* final update)
|
||||
* @param callback
|
||||
* @param minDelay
|
||||
* @returns {Function} a throttled version function
|
||||
*/
|
||||
export const throttle = (callback, minDelay = 1000) => {
|
||||
let lastParams
|
||||
let stalled = false
|
||||
let pending = false
|
||||
const invoke = (...params) => {
|
||||
lastParams = params
|
||||
if (stalled) {
|
||||
pending = true
|
||||
return
|
||||
}
|
||||
callback(...lastParams)
|
||||
stalled = true
|
||||
setTimeout(() => {
|
||||
stalled = false
|
||||
if (pending) {
|
||||
pending = false
|
||||
invoke(...lastParams)
|
||||
}
|
||||
}, minDelay)
|
||||
}
|
||||
return invoke
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue