commit
e369cbc6b3
|
@ -31,6 +31,12 @@ export const currentScreens = derived(store, $store => {
|
||||||
: Object.values(currentScreens)
|
: Object.values(currentScreens)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const selectedPage = derived(store, $store => {
|
||||||
|
if (!$store.pages) return null
|
||||||
|
|
||||||
|
return $store.pages[$store.currentPageName || "main"]
|
||||||
|
})
|
||||||
|
|
||||||
export const initialise = async () => {
|
export const initialise = async () => {
|
||||||
try {
|
try {
|
||||||
await analytics.activate()
|
await analytics.activate()
|
||||||
|
|
|
@ -5,8 +5,7 @@ import {
|
||||||
getBuiltin,
|
getBuiltin,
|
||||||
makePropsSafe,
|
makePropsSafe,
|
||||||
} from "components/userInterface/pagesParsing/createProps"
|
} from "components/userInterface/pagesParsing/createProps"
|
||||||
import { getExactComponent } from "components/userInterface/pagesParsing/searchComponents"
|
import { allScreens, backendUiStore, selectedPage } from "builderStore"
|
||||||
import { allScreens, backendUiStore } from "builderStore"
|
|
||||||
import { generate_screen_css } from "../generate_css"
|
import { generate_screen_css } from "../generate_css"
|
||||||
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
|
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
|
||||||
import api from "../api"
|
import api from "../api"
|
||||||
|
@ -37,6 +36,7 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
hasAppPackage: false,
|
hasAppPackage: false,
|
||||||
libraries: null,
|
libraries: null,
|
||||||
appId: "",
|
appId: "",
|
||||||
|
routes: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFrontendStore = () => {
|
export const getFrontendStore = () => {
|
||||||
|
@ -111,10 +111,9 @@ export const getFrontendStore = () => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.currentFrontEndType = type
|
state.currentFrontEndType = type
|
||||||
|
|
||||||
const pageOrScreen =
|
const page = get(selectedPage)
|
||||||
type === "page"
|
|
||||||
? state.pages[state.currentPageName]
|
const pageOrScreen = type === "page" ? page : page._screens[0]
|
||||||
: state.pages[state.currentPageName]._screens[0]
|
|
||||||
|
|
||||||
state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null
|
state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null
|
||||||
state.currentPreviewItem = pageOrScreen
|
state.currentPreviewItem = pageOrScreen
|
||||||
|
@ -122,10 +121,21 @@ export const getFrontendStore = () => {
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
screens: {
|
routing: {
|
||||||
select: screenName => {
|
fetch: async () => {
|
||||||
|
const response = await api.get("/api/routing")
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
const screen = getExactComponent(get(allScreens), screenName, true)
|
state.routes = json.routes
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
screens: {
|
||||||
|
select: screenId => {
|
||||||
|
store.update(state => {
|
||||||
|
const screen = get(allScreens).find(screen => screen._id === screenId)
|
||||||
state.currentPreviewItem = screen
|
state.currentPreviewItem = screen
|
||||||
state.currentFrontEndType = "screen"
|
state.currentFrontEndType = "screen"
|
||||||
state.currentView = "detail"
|
state.currentView = "detail"
|
||||||
|
@ -158,32 +168,25 @@ export const getFrontendStore = () => {
|
||||||
await savePromise
|
await savePromise
|
||||||
},
|
},
|
||||||
save: async screen => {
|
save: async screen => {
|
||||||
const storeContents = get(store)
|
const page = get(selectedPage)
|
||||||
const pageName = storeContents.currentPageName || "main"
|
const currentPageScreens = page._screens
|
||||||
const currentPage = storeContents.pages[pageName]
|
|
||||||
const currentPageScreens = currentPage._screens
|
|
||||||
|
|
||||||
const creatingNewScreen = screen._id === undefined
|
const creatingNewScreen = screen._id === undefined
|
||||||
|
|
||||||
let savePromise
|
let savePromise
|
||||||
const response = await api.post(
|
const response = await api.post(`/api/screens/${page._id}`, screen)
|
||||||
`/api/screens/${currentPage._id}`,
|
|
||||||
screen
|
|
||||||
)
|
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
screen._rev = json.rev
|
screen._rev = json.rev
|
||||||
screen._id = json.id
|
screen._id = json.id
|
||||||
const foundScreen = currentPageScreens.findIndex(
|
const foundScreen = page._screens.findIndex(el => el._id === screen._id)
|
||||||
el => el._id === screen._id
|
|
||||||
)
|
|
||||||
if (foundScreen !== -1) {
|
if (foundScreen !== -1) {
|
||||||
currentPageScreens.splice(foundScreen, 1)
|
page._screens.splice(foundScreen, 1)
|
||||||
}
|
}
|
||||||
currentPageScreens.push(screen)
|
page._screens.push(screen)
|
||||||
|
|
||||||
// TODO: should carry out all server updates to screen in a single call
|
// TODO: should carry out all server updates to screen in a single call
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.pages[pageName]._screens = currentPageScreens
|
page._screens = currentPageScreens
|
||||||
|
|
||||||
if (creatingNewScreen) {
|
if (creatingNewScreen) {
|
||||||
state.currentPreviewItem = screen
|
state.currentPreviewItem = screen
|
||||||
|
@ -209,21 +212,21 @@ export const getFrontendStore = () => {
|
||||||
store.actions.screens.regenerateCss(currentPreviewItem)
|
store.actions.screens.regenerateCss(currentPreviewItem)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
delete: async (screensToDelete, pageName) => {
|
delete: async screens => {
|
||||||
let deletePromise
|
let deletePromise
|
||||||
|
|
||||||
|
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
||||||
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
if (pageName == null) {
|
const currentPage = get(selectedPage)
|
||||||
pageName = state.pages.main.name
|
|
||||||
}
|
for (let screenToDelete of screensToDelete) {
|
||||||
for (let screenToDelete of Array.isArray(screensToDelete)
|
|
||||||
? screensToDelete
|
|
||||||
: [screensToDelete]) {
|
|
||||||
// Remove screen from current page as well
|
// Remove screen from current page as well
|
||||||
// TODO: Should be done server side
|
// TODO: Should be done server side
|
||||||
state.pages[pageName]._screens = state.pages[
|
currentPage._screens = currentPage._screens.filter(
|
||||||
pageName
|
scr => scr._id !== screenToDelete._id
|
||||||
]._screens.filter(scr => scr._id !== screenToDelete._id)
|
)
|
||||||
|
|
||||||
deletePromise = api.delete(
|
deletePromise = api.delete(
|
||||||
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
|
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
|
||||||
)
|
)
|
||||||
|
@ -309,14 +312,13 @@ export const getFrontendStore = () => {
|
||||||
create: (componentToAdd, presetProps) => {
|
create: (componentToAdd, presetProps) => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
function findSlot(component_array) {
|
function findSlot(component_array) {
|
||||||
for (let i = 0; i < component_array.length; i += 1) {
|
for (let component of component_array) {
|
||||||
if (component_array[i]._component === "##builtin/screenslot") {
|
if (component._component === "##builtin/screenslot") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (component_array[i]._children) findSlot(component_array[i])
|
if (component._children) findSlot(component)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,7 +379,7 @@ export const getFrontendStore = () => {
|
||||||
component._id
|
component._id
|
||||||
)
|
)
|
||||||
parent._children = parent._children.filter(
|
parent._children = parent._children.filter(
|
||||||
c => c._id !== component._id
|
child => child._id !== component._id
|
||||||
)
|
)
|
||||||
store.actions.components.select(parent)
|
store.actions.components.select(parent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@sveltech/routify"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { getComponentDefinition } from "builderStore/storeUtils"
|
||||||
|
import { DropEffect, DropPosition } from "./dragDropStore"
|
||||||
|
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
|
||||||
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
|
|
||||||
|
export let components = []
|
||||||
|
export let currentComponent
|
||||||
|
export let onSelect = () => {}
|
||||||
|
export let level = 0
|
||||||
|
|
||||||
|
export let dragDropStore
|
||||||
|
|
||||||
|
const isScreenslot = name => name === "##builtin/screenslot"
|
||||||
|
|
||||||
|
const selectComponent = component => {
|
||||||
|
// Set current component
|
||||||
|
store.actions.components.select(component)
|
||||||
|
|
||||||
|
// Get ID path
|
||||||
|
const path = store.actions.components.findRoute(component)
|
||||||
|
|
||||||
|
// Go to correct URL
|
||||||
|
$goto(`./:page/:screen/${path}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragstart = component => e => {
|
||||||
|
e.dataTransfer.dropEffect = DropEffect.MOVE
|
||||||
|
dragDropStore.actions.dragstart(component)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragover = (component, index) => e => {
|
||||||
|
const canHaveChildrenButIsEmpty =
|
||||||
|
getComponentDefinition($store, component._component).children &&
|
||||||
|
component._children.length === 0
|
||||||
|
|
||||||
|
e.dataTransfer.dropEffect = DropEffect.COPY
|
||||||
|
|
||||||
|
// how far down the mouse pointer is on the drop target
|
||||||
|
const mousePosition = e.offsetY / e.currentTarget.offsetHeight
|
||||||
|
|
||||||
|
dragDropStore.actions.dragover({
|
||||||
|
component,
|
||||||
|
index,
|
||||||
|
canHaveChildrenButIsEmpty,
|
||||||
|
mousePosition,
|
||||||
|
})
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{#each components as component, index (component._id)}
|
||||||
|
<li on:click|stopPropagation={() => selectComponent(component)}>
|
||||||
|
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
|
||||||
|
<div
|
||||||
|
on:drop={dragDropStore.actions.drop}
|
||||||
|
ondragover="return false"
|
||||||
|
ondragenter="return false"
|
||||||
|
class="drop-item"
|
||||||
|
style="margin-left: {(level + 1) * 16}px" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<NavItem
|
||||||
|
draggable
|
||||||
|
on:dragend={dragDropStore.actions.reset}
|
||||||
|
on:dragstart={dragstart(component)}
|
||||||
|
on:dragover={dragover(component, index)}
|
||||||
|
on:drop={dragDropStore.actions.drop}
|
||||||
|
text={isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
|
||||||
|
withArrow
|
||||||
|
indentLevel={level + 3}
|
||||||
|
selected={currentComponent === component}>
|
||||||
|
<ComponentDropdownMenu {component} />
|
||||||
|
</NavItem>
|
||||||
|
|
||||||
|
{#if component._children}
|
||||||
|
<svelte:self
|
||||||
|
components={component._children}
|
||||||
|
{currentComponent}
|
||||||
|
{onSelect}
|
||||||
|
{dragDropStore}
|
||||||
|
level={level + 1} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $dragDropStore?.targetComponent === component && ($dragDropStore.dropPosition === DropPosition.INSIDE || $dragDropStore.dropPosition === DropPosition.BELOW)}
|
||||||
|
<div
|
||||||
|
on:drop={dragDropStore.actions.drop}
|
||||||
|
ondragover="return false"
|
||||||
|
ondragenter="return false"
|
||||||
|
class="drop-item"
|
||||||
|
style="margin-left: {(level + ($dragDropStore.dropPosition === DropPosition.INSIDE ? 3 : 1)) * 16}px" />
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-item {
|
||||||
|
border-radius: var(--border-radius-m);
|
||||||
|
height: 32px;
|
||||||
|
background: var(--grey-3);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script>
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { goto } from "@sveltech/routify"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import instantiateStore from "./dragDropStore"
|
||||||
|
|
||||||
|
import ComponentsTree from "./ComponentTree.svelte"
|
||||||
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
|
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
||||||
|
|
||||||
|
const dragDropStore = instantiateStore()
|
||||||
|
|
||||||
|
export let route
|
||||||
|
export let path
|
||||||
|
export let indent
|
||||||
|
|
||||||
|
$: selectedScreen = $store.currentPreviewItem
|
||||||
|
|
||||||
|
const changeScreen = screenId => {
|
||||||
|
// select the route
|
||||||
|
store.actions.screens.select(screenId)
|
||||||
|
$goto(`./:page/${screenId}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NavItem
|
||||||
|
icon="ri-folder-line"
|
||||||
|
text={path}
|
||||||
|
opened={true}
|
||||||
|
withArrow={route.subpaths} />
|
||||||
|
|
||||||
|
{#each Object.entries(route.subpaths) as [url, subpath]}
|
||||||
|
{#each Object.values(subpath.screens) as screenId}
|
||||||
|
<NavItem
|
||||||
|
icon="ri-artboard-2-line"
|
||||||
|
indentLevel={indent || 1}
|
||||||
|
selected={$store.currentPreviewItem._id === screenId}
|
||||||
|
opened={$store.currentPreviewItem._id === screenId}
|
||||||
|
text={url === "/" ? "Home" : url}
|
||||||
|
withArrow={route.subpaths}
|
||||||
|
on:click={() => changeScreen(screenId)}>
|
||||||
|
<ScreenDropdownMenu screen={screenId} />
|
||||||
|
</NavItem>
|
||||||
|
{#if selectedScreen?._id === screenId}
|
||||||
|
<ComponentsTree
|
||||||
|
components={selectedScreen.props._children}
|
||||||
|
currentComponent={$store.currentComponentInfo}
|
||||||
|
{dragDropStore} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/each}
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto } from "@sveltech/routify"
|
||||||
import { store } from "builderStore"
|
import { store, allScreens } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { DropdownMenu } from "@budibase/bbui"
|
import { DropdownMenu } from "@budibase/bbui"
|
||||||
|
@ -13,12 +13,14 @@
|
||||||
let anchor
|
let anchor
|
||||||
|
|
||||||
const deleteScreen = () => {
|
const deleteScreen = () => {
|
||||||
store.actions.screens.delete(screen, $store.currentPageName)
|
const screenToDelete = $allScreens.find(scr => scr._id === screen)
|
||||||
|
store.actions.screens.delete(screenToDelete)
|
||||||
|
store.actions.routing.fetch()
|
||||||
// update the page if required
|
// update the page if required
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
if (state.currentPreviewItem.name === screen.name) {
|
if (state.currentPreviewItem._id === screen) {
|
||||||
store.actions.pages.select($store.currentPageName)
|
store.actions.pages.select($store.currentPageName)
|
||||||
notifier.success(`Screen ${screen.name} deleted successfully.`)
|
notifier.success(`Screen ${screenToDelete.name} deleted successfully.`)
|
||||||
$goto(`./:page/page-layout`)
|
$goto(`./:page/page-layout`)
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
|
@ -42,7 +44,7 @@
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:this={confirmDeleteDialog}
|
bind:this={confirmDeleteDialog}
|
||||||
title="Confirm Deletion"
|
title="Confirm Deletion"
|
||||||
body={`Are you sure you wish to delete the screen '${screen.props._instanceName}' ?`}
|
body={'Are you sure you wish to delete this screen?'}
|
||||||
okText="Delete Screen"
|
okText="Delete Screen"
|
||||||
onOk={deleteScreen} />
|
onOk={deleteScreen} />
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { store as frontendStore } from "builderStore"
|
||||||
|
|
||||||
|
export const DropEffect = {
|
||||||
|
MOVE: "move",
|
||||||
|
COPY: "copy",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DropPosition = {
|
||||||
|
ABOVE: "above",
|
||||||
|
BELOW: "below",
|
||||||
|
INSIDE: "inside",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
const store = writable({})
|
||||||
|
|
||||||
|
store.actions = {
|
||||||
|
dragstart: component => {
|
||||||
|
store.update(state => {
|
||||||
|
state.dragged = component
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
|
dragover: ({
|
||||||
|
component,
|
||||||
|
index,
|
||||||
|
canHaveChildrenButIsEmpty,
|
||||||
|
mousePosition,
|
||||||
|
}) => {
|
||||||
|
store.update(state => {
|
||||||
|
state.targetComponent = component
|
||||||
|
// only allow dropping inside when container is empty
|
||||||
|
// if container has children, drag over them
|
||||||
|
|
||||||
|
if (canHaveChildrenButIsEmpty && index === 0) {
|
||||||
|
// hovered above center of target
|
||||||
|
if (mousePosition < 0.4) {
|
||||||
|
state.dropPosition = DropPosition.ABOVE
|
||||||
|
}
|
||||||
|
|
||||||
|
// hovered around bottom of target
|
||||||
|
if (mousePosition > 0.8) {
|
||||||
|
state.dropPosition = DropPosition.BELOW
|
||||||
|
}
|
||||||
|
|
||||||
|
// hovered in center of target
|
||||||
|
if (mousePosition > 0.4 && mousePosition < 0.8) {
|
||||||
|
state.dropPosition = DropPosition.INSIDE
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// bottom half
|
||||||
|
if (mousePosition > 0.5) {
|
||||||
|
state.dropPosition = DropPosition.BELOW
|
||||||
|
} else {
|
||||||
|
state.dropPosition = canHaveChildrenButIsEmpty
|
||||||
|
? DropPosition.INSIDE
|
||||||
|
: DropPosition.ABOVE
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
|
reset: () => {
|
||||||
|
store.update(state => {
|
||||||
|
state.dropPosition = ""
|
||||||
|
state.targetComponent = null
|
||||||
|
state.dragged = null
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
|
drop: () => {
|
||||||
|
store.update(state => {
|
||||||
|
if (state.targetComponent !== state.dragged) {
|
||||||
|
frontendStore.actions.components.copy(state.dragged, true)
|
||||||
|
frontendStore.actions.components.paste(
|
||||||
|
state.targetComponent,
|
||||||
|
state.dropPosition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.actions.reset()
|
||||||
|
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return store
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@sveltech/routify"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import PathTree from "./PathTree.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
{#each Object.keys($store.routes) as path}
|
||||||
|
<PathTree {path} route={$store.routes[path]} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
|
@ -1,61 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto } from "@sveltech/routify"
|
|
||||||
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
|
|
||||||
import { trimCharsStart, trimChars } from "lodash/fp"
|
|
||||||
import { pipe } from "../../helpers"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
|
||||||
import { writable } from "svelte/store"
|
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
|
||||||
|
|
||||||
export let screens = []
|
|
||||||
|
|
||||||
$: sortedScreens = screens.sort((s1, s2) => {
|
|
||||||
const name1 = s1.props._instanceName?.toLowerCase() ?? ""
|
|
||||||
const name2 = s2.props._instanceName?.toLowerCase() ?? ""
|
|
||||||
return name1 > name2 ? 1 : -1
|
|
||||||
})
|
|
||||||
/*
|
|
||||||
Using a store here seems odd....
|
|
||||||
have a look in the <ComponentsHierarchyChildren /> code file to find out why.
|
|
||||||
I have commented the dragDropStore parameter
|
|
||||||
*/
|
|
||||||
const dragDropStore = writable({})
|
|
||||||
|
|
||||||
let confirmDeleteDialog
|
|
||||||
let componentToDelete = ""
|
|
||||||
|
|
||||||
const normalizedName = name =>
|
|
||||||
pipe(name, [
|
|
||||||
trimCharsStart("./"),
|
|
||||||
trimCharsStart("~/"),
|
|
||||||
trimCharsStart("../"),
|
|
||||||
trimChars(" "),
|
|
||||||
])
|
|
||||||
|
|
||||||
const changeScreen = screen => {
|
|
||||||
store.actions.screens.select(screen.props._instanceName)
|
|
||||||
$goto(`./:page/${screen.props._instanceName}`)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="root">
|
|
||||||
{#each sortedScreens as screen}
|
|
||||||
<NavItem
|
|
||||||
icon="ri-artboard-2-line"
|
|
||||||
text={screen.props._instanceName}
|
|
||||||
withArrow={screen.props._children.length}
|
|
||||||
selected={$store.currentComponentInfo._id === screen.props._id}
|
|
||||||
opened={$store.currentPreviewItem.name === screen.props._id}
|
|
||||||
on:click={() => changeScreen(screen)}>
|
|
||||||
<ScreenDropdownMenu {screen} />
|
|
||||||
</NavItem>
|
|
||||||
|
|
||||||
{#if $store.currentPreviewItem.props._instanceName && $store.currentPreviewItem.props._instanceName === screen.props._instanceName && screen.props._children}
|
|
||||||
<ComponentsHierarchyChildren
|
|
||||||
components={screen.props._children}
|
|
||||||
currentComponent={$store.currentComponentInfo}
|
|
||||||
{dragDropStore} />
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
|
@ -1,181 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto } from "@sveltech/routify"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import { last } from "lodash/fp"
|
|
||||||
import { pipe } from "../../helpers"
|
|
||||||
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
|
||||||
import { getComponentDefinition } from "builderStore/storeUtils"
|
|
||||||
|
|
||||||
export let components = []
|
|
||||||
export let currentComponent
|
|
||||||
export let onSelect = () => {}
|
|
||||||
export let level = 0
|
|
||||||
|
|
||||||
/*
|
|
||||||
"dragDropStore" is a svelte store.
|
|
||||||
This component is recursive... a tree.
|
|
||||||
Using a single, shared store, all the nodes in the tree can subscribe to state that is changed by other nodes, in the same tree.
|
|
||||||
|
|
||||||
e.g. Say i have the structure
|
|
||||||
- Heading 1
|
|
||||||
- Container
|
|
||||||
- Heading 2
|
|
||||||
- Heading 3
|
|
||||||
- Heading 4
|
|
||||||
|
|
||||||
1. When I dragover "Heading 1", a placeholder drop-slot appears below it
|
|
||||||
2. I drag down a bit so the cursor is INSIDE the container (i.e. now in a child <ComponentsHierarchyChildren />)
|
|
||||||
3. Using store subscribes... the original drop-slot now knows that it should disappear, and a new one is created inside the container.
|
|
||||||
*/
|
|
||||||
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 isScreenslot = name => name === "##builtin/screenslot"
|
|
||||||
|
|
||||||
const selectComponent = component => {
|
|
||||||
// Set current component
|
|
||||||
store.actions.components.select(component)
|
|
||||||
|
|
||||||
// Get ID path
|
|
||||||
const path = store.actions.components.findRoute(component)
|
|
||||||
|
|
||||||
// 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 =
|
|
||||||
getComponentDefinition($store, 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.actions.components.copy($dragDropStore.componentToDrop, true)
|
|
||||||
store.actions.components.paste(
|
|
||||||
$dragDropStore.targetComponent,
|
|
||||||
$dragDropStore.dropPosition
|
|
||||||
)
|
|
||||||
}
|
|
||||||
dragDropStore.update(s => {
|
|
||||||
s.dropPosition = ""
|
|
||||||
s.targetComponent = null
|
|
||||||
s.componentToDrop = null
|
|
||||||
return s
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const dragend = () => {
|
|
||||||
dragDropStore.update(s => {
|
|
||||||
s.dropPosition = ""
|
|
||||||
s.targetComponent = null
|
|
||||||
s.componentToDrop = null
|
|
||||||
return s
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{#each components as component, index (component._id)}
|
|
||||||
<li on:click|stopPropagation={() => selectComponent(component)}>
|
|
||||||
{#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition === 'above'}
|
|
||||||
<div
|
|
||||||
on:drop={drop}
|
|
||||||
ondragover="return false"
|
|
||||||
ondragenter="return false"
|
|
||||||
class="drop-item"
|
|
||||||
style="margin-left: {(level + 1) * 16}px" />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<NavItem
|
|
||||||
draggable
|
|
||||||
on:dragend={dragend}
|
|
||||||
on:dragstart={dragstart(component)}
|
|
||||||
on:dragover={dragover(component, index)}
|
|
||||||
on:drop={drop}
|
|
||||||
text={isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
|
|
||||||
withArrow
|
|
||||||
indentLevel={level + 1}
|
|
||||||
selected={currentComponent === component}>
|
|
||||||
<ComponentDropdownMenu {component} />
|
|
||||||
</NavItem>
|
|
||||||
|
|
||||||
{#if component._children}
|
|
||||||
<svelte:self
|
|
||||||
components={component._children}
|
|
||||||
{currentComponent}
|
|
||||||
{onSelect}
|
|
||||||
{dragDropStore}
|
|
||||||
level={level + 1} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $dragDropStore && $dragDropStore.targetComponent === component && ($dragDropStore.dropPosition === 'inside' || $dragDropStore.dropPosition === 'below')}
|
|
||||||
<div
|
|
||||||
on:drop={drop}
|
|
||||||
ondragover="return false"
|
|
||||||
ondragenter="return false"
|
|
||||||
class="drop-item"
|
|
||||||
style="margin-left: {(level + ($dragDropStore.dropPosition === 'inside' ? 3 : 1)) * 16}px" />
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding-left: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-item {
|
|
||||||
border-radius: var(--border-radius-m);
|
|
||||||
height: 32px;
|
|
||||||
background: var(--grey-3);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,12 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
import { store, currentScreens } from "builderStore"
|
import { store, currentScreens } from "builderStore"
|
||||||
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte"
|
import api from "builderStore/api"
|
||||||
|
import ComponentNavigationTree from "components/userInterface/ComponentNavigationTree/index.svelte"
|
||||||
import PageLayout from "components/userInterface/PageLayout.svelte"
|
import PageLayout from "components/userInterface/PageLayout.svelte"
|
||||||
import PagesList from "components/userInterface/PagesList.svelte"
|
import PagesList from "components/userInterface/PagesList.svelte"
|
||||||
import NewScreenModal from "components/userInterface/NewScreenModal.svelte"
|
import NewScreenModal from "components/userInterface/NewScreenModal.svelte"
|
||||||
import { Modal } from "@budibase/bbui"
|
import { Modal } from "@budibase/bbui"
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
|
let routes = {}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
store.actions.routing.fetch()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
@ -16,7 +24,7 @@
|
||||||
<PagesList />
|
<PagesList />
|
||||||
<div class="nav-items-container">
|
<div class="nav-items-container">
|
||||||
<PageLayout layout={$store.pages[$store.currentPageName]} />
|
<PageLayout layout={$store.pages[$store.currentPageName]} />
|
||||||
<ComponentsHierarchy screens={$currentScreens} />
|
<ComponentNavigationTree />
|
||||||
</div>
|
</div>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<NewScreenModal />
|
<NewScreenModal />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto } from "@sveltech/routify"
|
||||||
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
|
import ComponentTree from "./ComponentNavigationTree/ComponentTree.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import { last } from "lodash/fp"
|
import { last } from "lodash/fp"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
@ -37,8 +37,7 @@
|
||||||
on:click={setCurrentScreenToLayout} />
|
on:click={setCurrentScreenToLayout} />
|
||||||
|
|
||||||
{#if $store.currentPreviewItem?.name === _layout.title && _layout.component.props._children}
|
{#if $store.currentPreviewItem?.name === _layout.title && _layout.component.props._children}
|
||||||
<ComponentsHierarchyChildren
|
<ComponentTree
|
||||||
thisComponent={_layout.component.props}
|
|
||||||
components={_layout.component.props._children}
|
components={_layout.component.props._children}
|
||||||
currentComponent={$store.currentComponentInfo}
|
currentComponent={$store.currentComponentInfo}
|
||||||
{dragDropStore} />
|
{dragDropStore} />
|
||||||
|
|
|
@ -10,9 +10,7 @@
|
||||||
if ($params.screen !== "page-layout") {
|
if ($params.screen !== "page-layout") {
|
||||||
const currentScreenName = decodeURI($params.screen)
|
const currentScreenName = decodeURI($params.screen)
|
||||||
const validScreen =
|
const validScreen =
|
||||||
$allScreens.findIndex(
|
$allScreens.findIndex(screen => screen._id === currentScreenName) !== -1
|
||||||
screen => screen.props._instanceName === currentScreenName
|
|
||||||
) !== -1
|
|
||||||
|
|
||||||
if (!validScreen) {
|
if (!validScreen) {
|
||||||
// Go to main layout if URL set to invalid screen
|
// Go to main layout if URL set to invalid screen
|
||||||
|
@ -27,8 +25,8 @@
|
||||||
// Get the correct screen children.
|
// Get the correct screen children.
|
||||||
const screenChildren = $store.pages[$params.page]._screens.find(
|
const screenChildren = $store.pages[$params.page]._screens.find(
|
||||||
screen =>
|
screen =>
|
||||||
screen.props._instanceName === $params.screen ||
|
screen._id === $params.screen ||
|
||||||
screen.props._instanceName === decodeURIComponent($params.screen)
|
screen._id === decodeURIComponent($params.screen)
|
||||||
).props._children
|
).props._children
|
||||||
findComponent(componentIds, screenChildren)
|
findComponent(componentIds, screenChildren)
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
"rollup-plugin-node-globals": "^1.4.0",
|
"rollup-plugin-node-globals": "^1.4.0",
|
||||||
"rollup-plugin-node-resolve": "^5.2.0",
|
"rollup-plugin-node-resolve": "^5.2.0",
|
||||||
"rollup-plugin-terser": "^4.0.4",
|
"rollup-plugin-terser": "^4.0.4",
|
||||||
"svelte": "3.23.x",
|
"svelte": "^3.29.7",
|
||||||
"svelte-jester": "^1.0.6"
|
"svelte-jester": "^1.0.6"
|
||||||
},
|
},
|
||||||
"gitHead": "e4e053cb6ff9a0ddc7115b44ccaa24b8ec41fb9a"
|
"gitHead": "e4e053cb6ff9a0ddc7115b44ccaa24b8ec41fb9a"
|
||||||
|
|
|
@ -43,7 +43,7 @@ export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
||||||
return sanitize(url)
|
return sanitize(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = screens.map(s => makeRootedPath(s.routing?.route))
|
const routes = screens.map(screen => makeRootedPath(screen?.routing.route))
|
||||||
let fallback = routes.findIndex(([p]) => p === makeRootedPath("*"))
|
let fallback = routes.findIndex(([p]) => p === makeRootedPath("*"))
|
||||||
if (fallback < 0) fallback = 0
|
if (fallback < 0) fallback = 0
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue