Merge pull request #11583 from Budibase/design-section-feature-branch
New design section
This commit is contained in:
commit
b6db5875ee
|
@ -32,11 +32,10 @@ export default function positionDropdown(element, opts) {
|
||||||
left: null,
|
left: null,
|
||||||
top: null,
|
top: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine vertical styles
|
// Determine vertical styles
|
||||||
if (align === "right-outside") {
|
if (align === "right-outside") {
|
||||||
styles.top = anchorBounds.top
|
styles.top = anchorBounds.top
|
||||||
} else if (window.innerHeight - anchorBounds.bottom < 100) {
|
} else if (window.innerHeight - anchorBounds.bottom < (maxHeight || 100)) {
|
||||||
styles.top = anchorBounds.top - elementBounds.height - offset
|
styles.top = anchorBounds.top - elementBounds.height - offset
|
||||||
styles.maxHeight = maxHeight || 240
|
styles.maxHeight = maxHeight || 240
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
|
import Popover from "../Popover/Popover.svelte"
|
||||||
|
import Layout from "../Layout/Layout.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import "@spectrum-css/popover/dist/index-vars.css"
|
import "@spectrum-css/popover/dist/index-vars.css"
|
||||||
import clickOutside from "../Actions/click_outside"
|
|
||||||
import { fly } from "svelte/transition"
|
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
import Input from "../Form/Input.svelte"
|
import Input from "../Form/Input.svelte"
|
||||||
import { capitalise } from "../helpers"
|
import { capitalise } from "../helpers"
|
||||||
|
@ -10,9 +10,11 @@
|
||||||
export let value
|
export let value
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let spectrumTheme
|
export let spectrumTheme
|
||||||
export let alignRight = false
|
export let offset
|
||||||
|
export let align
|
||||||
|
|
||||||
let open = false
|
let dropdown
|
||||||
|
let preview
|
||||||
|
|
||||||
$: customValue = getCustomValue(value)
|
$: customValue = getCustomValue(value)
|
||||||
$: checkColor = getCheckColor(value)
|
$: checkColor = getCheckColor(value)
|
||||||
|
@ -82,7 +84,7 @@
|
||||||
|
|
||||||
const onChange = value => {
|
const onChange = value => {
|
||||||
dispatch("change", value)
|
dispatch("change", value)
|
||||||
open = false
|
dropdown.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomValue = value => {
|
const getCustomValue = value => {
|
||||||
|
@ -119,30 +121,25 @@
|
||||||
|
|
||||||
return "var(--spectrum-global-color-static-gray-900)"
|
return "var(--spectrum-global-color-static-gray-900)"
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOutsideClick = event => {
|
|
||||||
if (open) {
|
|
||||||
event.stopPropagation()
|
|
||||||
open = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div
|
||||||
<div class="preview size--{size || 'M'}" on:click={() => (open = true)}>
|
bind:this={preview}
|
||||||
<div
|
class="preview size--{size || 'M'}"
|
||||||
class="fill {spectrumTheme || ''}"
|
on:click={() => {
|
||||||
style={value ? `background: ${value};` : ""}
|
dropdown.toggle()
|
||||||
class:placeholder={!value}
|
}}
|
||||||
/>
|
>
|
||||||
</div>
|
<div
|
||||||
{#if open}
|
class="fill {spectrumTheme || ''}"
|
||||||
<div
|
style={value ? `background: ${value};` : ""}
|
||||||
use:clickOutside={handleOutsideClick}
|
class:placeholder={!value}
|
||||||
transition:fly|local={{ y: -20, duration: 200 }}
|
/>
|
||||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
</div>
|
||||||
class:spectrum-Popover--align-right={alignRight}
|
|
||||||
>
|
<Popover bind:this={dropdown} anchor={preview} maxHeight={320} {offset} {align}>
|
||||||
|
<Layout paddingX="XL" paddingY="L">
|
||||||
|
<div class="container">
|
||||||
{#each categories as category}
|
{#each categories as category}
|
||||||
<div class="category">
|
<div class="category">
|
||||||
<div class="heading">{category.label}</div>
|
<div class="heading">{category.label}</div>
|
||||||
|
@ -187,8 +184,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</Layout>
|
||||||
</div>
|
</Popover>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
|
@ -248,20 +245,6 @@
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
.spectrum-Popover {
|
|
||||||
width: 210px;
|
|
||||||
z-index: 999;
|
|
||||||
top: 100%;
|
|
||||||
padding: var(--spacing-l) var(--spacing-xl);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.spectrum-Popover--align-right {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
.colors {
|
.colors {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||||
|
@ -297,7 +280,11 @@
|
||||||
.category--custom .heading {
|
.category--custom .heading {
|
||||||
margin-bottom: var(--spacing-xs);
|
margin-bottom: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
.spectrum-wrapper {
|
.spectrum-wrapper {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,14 @@
|
||||||
open = false
|
open = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const toggle = () => {
|
||||||
|
if (!open) {
|
||||||
|
show()
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleOutsideClick = e => {
|
const handleOutsideClick = e => {
|
||||||
if (open) {
|
if (open) {
|
||||||
// Stop propagation if the source is the anchor
|
// Stop propagation if the source is the anchor
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getTemporalStore } from "./store/temporal"
|
||||||
import { getThemeStore } from "./store/theme"
|
import { getThemeStore } from "./store/theme"
|
||||||
import { getUserStore } from "./store/users"
|
import { getUserStore } from "./store/users"
|
||||||
import { getDeploymentStore } from "./store/deployments"
|
import { getDeploymentStore } from "./store/deployments"
|
||||||
import { derived } from "svelte/store"
|
import { derived, writable } from "svelte/store"
|
||||||
import { findComponent, findComponentPath } from "./componentUtils"
|
import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
import { createHistoryStore } from "builderStore/store/history"
|
import { createHistoryStore } from "builderStore/store/history"
|
||||||
|
@ -61,6 +61,12 @@ export const selectedLayout = derived(store, $store => {
|
||||||
export const selectedComponent = derived(
|
export const selectedComponent = derived(
|
||||||
[store, selectedScreen],
|
[store, selectedScreen],
|
||||||
([$store, $selectedScreen]) => {
|
([$store, $selectedScreen]) => {
|
||||||
|
if (
|
||||||
|
$selectedScreen &&
|
||||||
|
$store.selectedComponentId?.startsWith(`${$selectedScreen._id}-`)
|
||||||
|
) {
|
||||||
|
return $selectedScreen?.props
|
||||||
|
}
|
||||||
if (!$selectedScreen || !$store.selectedComponentId) {
|
if (!$selectedScreen || !$store.selectedComponentId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -141,3 +147,5 @@ export const userSelectedResourceMap = derived(userStore, $userStore => {
|
||||||
export const isOnlyUser = derived(userStore, $userStore => {
|
export const isOnlyUser = derived(userStore, $userStore => {
|
||||||
return $userStore.length < 2
|
return $userStore.length < 2
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const screensHeight = writable("210px")
|
||||||
|
|
|
@ -225,7 +225,6 @@ export const getFrontendStore = () => {
|
||||||
// Select new screen
|
// Select new screen
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.selectedScreenId = screen._id
|
state.selectedScreenId = screen._id
|
||||||
state.selectedComponentId = screen.props?._id
|
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -769,9 +768,13 @@ export const getFrontendStore = () => {
|
||||||
else {
|
else {
|
||||||
await store.actions.screens.patch(screen => {
|
await store.actions.screens.patch(screen => {
|
||||||
// Find the selected component
|
// Find the selected component
|
||||||
|
let selectedComponentId = state.selectedComponentId
|
||||||
|
if (selectedComponentId.startsWith(`${screen._id}-`)) {
|
||||||
|
selectedComponentId = screen?.props._id
|
||||||
|
}
|
||||||
const currentComponent = findComponent(
|
const currentComponent = findComponent(
|
||||||
screen.props,
|
screen.props,
|
||||||
state.selectedComponentId
|
selectedComponentId
|
||||||
)
|
)
|
||||||
if (!currentComponent) {
|
if (!currentComponent) {
|
||||||
return false
|
return false
|
||||||
|
@ -994,12 +997,20 @@ export const getFrontendStore = () => {
|
||||||
const componentId = state.selectedComponentId
|
const componentId = state.selectedComponentId
|
||||||
const screen = get(selectedScreen)
|
const screen = get(selectedScreen)
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
|
|
||||||
// Check we aren't right at the top of the tree
|
|
||||||
const index = parent?._children.findIndex(x => x._id === componentId)
|
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||||
if (!parent || componentId === screen.props._id) {
|
|
||||||
|
// Check for screen and navigation component edge cases
|
||||||
|
const screenComponentId = `${screen._id}-screen`
|
||||||
|
const navComponentId = `${screen._id}-navigation`
|
||||||
|
if (componentId === screenComponentId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
if (componentId === navComponentId) {
|
||||||
|
return screenComponentId
|
||||||
|
}
|
||||||
|
if (parent._id === screen.props._id && index === 0) {
|
||||||
|
return navComponentId
|
||||||
|
}
|
||||||
|
|
||||||
// If we have siblings above us, choose the sibling or a descendant
|
// If we have siblings above us, choose the sibling or a descendant
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
|
@ -1021,12 +1032,20 @@ export const getFrontendStore = () => {
|
||||||
return parent._id
|
return parent._id
|
||||||
},
|
},
|
||||||
getNext: () => {
|
getNext: () => {
|
||||||
|
const state = get(store)
|
||||||
const component = get(selectedComponent)
|
const component = get(selectedComponent)
|
||||||
const componentId = component?._id
|
const componentId = component?._id
|
||||||
const screen = get(selectedScreen)
|
const screen = get(selectedScreen)
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
const index = parent?._children.findIndex(x => x._id === componentId)
|
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||||
|
|
||||||
|
// Check for screen and navigation component edge cases
|
||||||
|
const screenComponentId = `${screen._id}-screen`
|
||||||
|
const navComponentId = `${screen._id}-navigation`
|
||||||
|
if (state.selectedComponentId === screenComponentId) {
|
||||||
|
return navComponentId
|
||||||
|
}
|
||||||
|
|
||||||
// If we have children, select first child
|
// If we have children, select first child
|
||||||
if (component._children?.length) {
|
if (component._children?.length) {
|
||||||
return component._children[0]._id
|
return component._children[0]._id
|
||||||
|
|
|
@ -121,7 +121,9 @@
|
||||||
type: "Screen",
|
type: "Screen",
|
||||||
name: screen.routing.route,
|
name: screen.routing.route,
|
||||||
icon: "WebPage",
|
icon: "WebPage",
|
||||||
action: () => $goto(`./design/${screen._id}/components`),
|
action: () => {
|
||||||
|
$goto(`./design/${screen._id}/${screen._id}-screen`)
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
...($automationStore?.automations?.map(automation => ({
|
...($automationStore?.automations?.map(automation => ({
|
||||||
type: "Automation",
|
type: "Automation",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
export let id
|
export let id
|
||||||
export let showTooltip = false
|
export let showTooltip = false
|
||||||
export let selectedBy = null
|
export let selectedBy = null
|
||||||
|
export let compact = false
|
||||||
|
|
||||||
const scrollApi = getContext("scroll")
|
const scrollApi = getContext("scroll")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -80,8 +81,9 @@
|
||||||
{#if withArrow}
|
{#if withArrow}
|
||||||
<div
|
<div
|
||||||
class:opened
|
class:opened
|
||||||
class:relative={indentLevel === 0}
|
class:relative={indentLevel === 0 && !compact}
|
||||||
class:absolute={indentLevel > 0}
|
class:absolute={indentLevel > 0 && !compact}
|
||||||
|
class:compact
|
||||||
class="icon arrow"
|
class="icon arrow"
|
||||||
on:click={onIconClick}
|
on:click={onIconClick}
|
||||||
>
|
>
|
||||||
|
@ -194,10 +196,21 @@
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin-left: -8px;
|
margin-left: -8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compact {
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
margin-left: -8px;
|
||||||
|
}
|
||||||
.icon.arrow :global(svg) {
|
.icon.arrow :global(svg) {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
.icon.arrow.compact :global(svg) {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
}
|
||||||
.icon.arrow.relative {
|
.icon.arrow.relative {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 -6px 0 -4px;
|
margin: 0 -6px 0 -4px;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Heading } from "@budibase/bbui"
|
import { Icon, Body } from "@budibase/bbui"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let icon
|
export let icon
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
<Icon name={icon} />
|
<Icon name={icon} />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<Heading size="XXS">{title || ""}</Heading>
|
<Body size="S">{title}</Body>
|
||||||
</div>
|
</div>
|
||||||
{#if showAddButton}
|
{#if showAddButton}
|
||||||
<div class="add-button" on:click={onClickAddButton}>
|
<div class="add-button" on:click={onClickAddButton}>
|
||||||
|
@ -78,15 +78,14 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 var(--spacing-l);
|
padding: 0 var(--spacing-l);
|
||||||
border-bottom: var(--border-light);
|
border-bottom: var(--border-light);
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
.title :global(h1) {
|
.title :global(p) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-weight: 600;
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,6 @@
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
value={column.background}
|
value={column.background}
|
||||||
on:change={e => (column.background = e.detail)}
|
on:change={e => (column.background = e.detail)}
|
||||||
alignRight
|
|
||||||
spectrumTheme={$store.theme}
|
spectrumTheme={$store.theme}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -51,7 +50,6 @@
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
value={column.color}
|
value={column.color}
|
||||||
on:change={e => (column.color = e.detail)}
|
on:change={e => (column.color = e.detail)}
|
||||||
alignRight
|
|
||||||
spectrumTheme={$store.theme}
|
spectrumTheme={$store.theme}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -56,6 +56,11 @@ export const syncURLToState = options => {
|
||||||
|
|
||||||
// Navigate to a certain URL
|
// Navigate to a certain URL
|
||||||
const gotoUrl = (url, params) => {
|
const gotoUrl = (url, params) => {
|
||||||
|
// Clean URL
|
||||||
|
if (url?.endsWith("/index")) {
|
||||||
|
url = url.replace("/index", "")
|
||||||
|
}
|
||||||
|
// Allow custom URL handling
|
||||||
if (beforeNavigate) {
|
if (beforeNavigate) {
|
||||||
const res = beforeNavigate(url, params)
|
const res = beforeNavigate(url, params)
|
||||||
if (res?.url) {
|
if (res?.url) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Drawer } from "@budibase/bbui"
|
import { Button, Drawer } from "@budibase/bbui"
|
||||||
import NavigationLinksDrawer from "./NavigationLinksDrawer.svelte"
|
import NavigationLinksDrawer from "./LinksDrawer.svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
|
||||||
|
@ -20,12 +20,8 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button cta on:click={openDrawer}>Configure links</Button>
|
<Button cta on:click={openDrawer}>Configure Links</Button>
|
||||||
<Drawer
|
<Drawer bind:this={drawer} title={"Navigation Links"}>
|
||||||
bind:this={drawer}
|
|
||||||
title={"Navigation Links"}
|
|
||||||
width="calc(100% - 334px)"
|
|
||||||
>
|
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Configure the links in your navigation bar.
|
Configure the links in your navigation bar.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
|
@ -0,0 +1,225 @@
|
||||||
|
<script>
|
||||||
|
import LinksEditor from "./LinksEditor.svelte"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import Panel from "components/design/Panel.svelte"
|
||||||
|
import {
|
||||||
|
Detail,
|
||||||
|
Toggle,
|
||||||
|
Body,
|
||||||
|
Icon,
|
||||||
|
ColorPicker,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
ActionGroup,
|
||||||
|
ActionButton,
|
||||||
|
Checkbox,
|
||||||
|
notifications,
|
||||||
|
Select,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { selectedScreen, store } from "builderStore"
|
||||||
|
import { DefaultAppTheme } from "constants"
|
||||||
|
|
||||||
|
const updateShowNavigation = async e => {
|
||||||
|
await store.actions.screens.updateSetting(
|
||||||
|
get(selectedScreen),
|
||||||
|
"showNavigation",
|
||||||
|
e.detail
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = async (key, value) => {
|
||||||
|
try {
|
||||||
|
let navigation = $store.navigation
|
||||||
|
navigation[key] = value
|
||||||
|
await store.actions.navigation.save(navigation)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error updating navigation settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Panel
|
||||||
|
title="Navigation"
|
||||||
|
icon={$selectedScreen.showNavigation ? "Visibility" : "VisibilityOff"}
|
||||||
|
borderLeft
|
||||||
|
wide
|
||||||
|
>
|
||||||
|
<div class="generalSection">
|
||||||
|
<div class="subheading">
|
||||||
|
<Detail>General</Detail>
|
||||||
|
</div>
|
||||||
|
<div class="toggle">
|
||||||
|
<Toggle
|
||||||
|
on:change={updateShowNavigation}
|
||||||
|
value={$selectedScreen.showNavigation}
|
||||||
|
/>
|
||||||
|
<Body size="S">Show nav on this screen</Body>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $selectedScreen.showNavigation}
|
||||||
|
<div class="divider" />
|
||||||
|
<div class="customizeSection">
|
||||||
|
<div class="subheading">
|
||||||
|
<Detail>Customize</Detail>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<Icon name="InfoOutline" size="S" />
|
||||||
|
<Body size="S">These settings apply to all screens</Body>
|
||||||
|
</div>
|
||||||
|
<div class="configureLinks">
|
||||||
|
<LinksEditor />
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Position</Label>
|
||||||
|
</div>
|
||||||
|
<ActionGroup quiet>
|
||||||
|
<ActionButton
|
||||||
|
selected={$store.navigation.navigation === "Top"}
|
||||||
|
quiet={$store.navigation.navigation !== "Top"}
|
||||||
|
icon="PaddingTop"
|
||||||
|
on:click={() => update("navigation", "Top")}
|
||||||
|
/>
|
||||||
|
<ActionButton
|
||||||
|
selected={$store.navigation.navigation === "Left"}
|
||||||
|
quiet={$store.navigation.navigation !== "Left"}
|
||||||
|
icon="PaddingLeft"
|
||||||
|
on:click={() => update("navigation", "Left")}
|
||||||
|
/>
|
||||||
|
</ActionGroup>
|
||||||
|
|
||||||
|
{#if $store.navigation.navigation === "Top"}
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Sticky header</Label>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
value={$store.navigation.sticky}
|
||||||
|
on:change={e => update("sticky", e.detail)}
|
||||||
|
/>
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Width</Label>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
options={["Max", "Large", "Medium", "Small"]}
|
||||||
|
plaveholder={null}
|
||||||
|
value={$store.navigation.navWidth}
|
||||||
|
on:change={e => update("navWidth", e.detail)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Show logo</Label>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
value={!$store.navigation.hideLogo}
|
||||||
|
on:change={e => update("hideLogo", !e.detail)}
|
||||||
|
/>
|
||||||
|
{#if !$store.navigation.hideLogo}
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Logo URL</Label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={$store.navigation.logoUrl}
|
||||||
|
on:change={e => update("logoUrl", e.detail)}
|
||||||
|
updateOnChange={false}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Show title</Label>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
value={!$store.navigation.hideTitle}
|
||||||
|
on:change={e => update("hideTitle", !e.detail)}
|
||||||
|
/>
|
||||||
|
{#if !$store.navigation.hideTitle}
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Title</Label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={$store.navigation.title}
|
||||||
|
on:change={e => update("title", e.detail)}
|
||||||
|
updateOnChange={false}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="label">
|
||||||
|
<Label>Background</Label>
|
||||||
|
</div>
|
||||||
|
<ColorPicker
|
||||||
|
spectrumTheme={$store.theme}
|
||||||
|
value={$store.navigation.navBackground ||
|
||||||
|
DefaultAppTheme.navBackground}
|
||||||
|
on:change={e => update("navBackground", e.detail)}
|
||||||
|
/>
|
||||||
|
<div class="label">
|
||||||
|
<Label>Text</Label>
|
||||||
|
</div>
|
||||||
|
<ColorPicker
|
||||||
|
spectrumTheme={$store.theme}
|
||||||
|
value={$store.navigation.navTextColor || DefaultAppTheme.navTextColor}
|
||||||
|
on:change={e => update("navTextColor", e.detail)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.generalSection {
|
||||||
|
padding: 13px 13px 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customizeSection {
|
||||||
|
padding: 13px 13px 25px;
|
||||||
|
}
|
||||||
|
.subheading {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subheading :global(p) {
|
||||||
|
color: var(--grey-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
border-top: 1px solid var(--grey-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 90px 1fr;
|
||||||
|
align-items: start;
|
||||||
|
transition: background 130ms ease-out, border-color 130ms ease-out;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
margin: 0 calc(-1 * var(--spacing-xl));
|
||||||
|
padding: 0 var(--spacing-xl) 0 calc(var(--spacing-xl) - 4px);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-top: 16px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
background-color: var(--background-alt);
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 4px;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.info :global(svg) {
|
||||||
|
margin-right: 5px;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.configureLinks :global(button) {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,12 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import Panel from "components/design/Panel.svelte"
|
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
Layout,
|
|
||||||
Button,
|
|
||||||
Toggle,
|
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Banner,
|
Banner,
|
||||||
Select,
|
Select,
|
||||||
|
@ -16,7 +12,6 @@
|
||||||
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
||||||
import { selectedScreen, store } from "builderStore"
|
import { selectedScreen, store } from "builderStore"
|
||||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import ButtonActionEditor from "components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte"
|
import ButtonActionEditor from "components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte"
|
||||||
import { getBindableProperties } from "builderStore/dataBinding"
|
import { getBindableProperties } from "builderStore/dataBinding"
|
||||||
|
|
||||||
|
@ -119,15 +114,6 @@
|
||||||
label: "On screen load",
|
label: "On screen load",
|
||||||
control: ButtonActionEditor,
|
control: ButtonActionEditor,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "showNavigation",
|
|
||||||
label: "Navigation",
|
|
||||||
control: Toggle,
|
|
||||||
props: {
|
|
||||||
text: "Show nav",
|
|
||||||
disabled: !!$selectedScreen.layoutId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "width",
|
key: "width",
|
||||||
label: "Width",
|
label: "Width",
|
||||||
|
@ -145,36 +131,24 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel
|
{#if $selectedScreen.layoutId}
|
||||||
title={$selectedScreen.routing.route}
|
<Banner
|
||||||
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
|
type="warning"
|
||||||
borderLeft
|
extraButtonText="Detach custom layout"
|
||||||
wide
|
extraButtonAction={removeCustomLayout}
|
||||||
>
|
showCloseButton={false}
|
||||||
<Layout gap="S" paddingX="L" paddingY="XL">
|
>
|
||||||
{#if $selectedScreen.layoutId}
|
This screen uses a custom layout, which is deprecated
|
||||||
<Banner
|
</Banner>
|
||||||
type="warning"
|
{/if}
|
||||||
extraButtonText="Detach custom layout"
|
{#each screenSettings as setting (setting.key)}
|
||||||
extraButtonAction={removeCustomLayout}
|
<PropertyControl
|
||||||
showCloseButton={false}
|
control={setting.control}
|
||||||
>
|
label={setting.label}
|
||||||
This screen uses a custom layout, which is deprecated
|
key={setting.key}
|
||||||
</Banner>
|
value={Helpers.deepGet($selectedScreen, setting.key)}
|
||||||
{/if}
|
onChange={val => setScreenSetting(setting, val)}
|
||||||
{#each screenSettings as setting (setting.key)}
|
props={{ ...setting.props, error: errors[setting.key] }}
|
||||||
<PropertyControl
|
{bindings}
|
||||||
control={setting.control}
|
/>
|
||||||
label={setting.label}
|
{/each}
|
||||||
key={setting.key}
|
|
||||||
value={Helpers.deepGet($selectedScreen, setting.key)}
|
|
||||||
onChange={val => setScreenSetting(setting, val)}
|
|
||||||
props={{ ...setting.props, error: errors[setting.key] }}
|
|
||||||
{bindings}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
<Button secondary on:click={() => $goto("../components")}>
|
|
||||||
View components
|
|
||||||
</Button>
|
|
||||||
</Layout>
|
|
||||||
</Panel>
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Label,
|
||||||
|
ColorPicker,
|
||||||
|
notifications,
|
||||||
|
Icon,
|
||||||
|
Body,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { DefaultAppTheme } from "constants"
|
||||||
|
import AppThemeSelect from "./AppThemeSelect.svelte"
|
||||||
|
import ButtonRoundnessSelect from "./ButtonRoundnessSelect.svelte"
|
||||||
|
import PropertyControl from "components/design/settings/controls/PropertyControl.svelte"
|
||||||
|
|
||||||
|
$: customTheme = $store.customTheme || {}
|
||||||
|
|
||||||
|
const update = async (property, value) => {
|
||||||
|
try {
|
||||||
|
store.actions.customTheme.save({
|
||||||
|
...get(store).customTheme,
|
||||||
|
[property]: value,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error updating custom theme")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<Icon name="InfoOutline" size="S" />
|
||||||
|
<Body size="S">These settings apply to all screens</Body>
|
||||||
|
</div>
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<AppThemeSelect />
|
||||||
|
</Layout>
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<Label>Button roundness</Label>
|
||||||
|
<ButtonRoundnessSelect
|
||||||
|
{customTheme}
|
||||||
|
on:change={e => update("buttonBorderRadius", e.detail)}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
<PropertyControl
|
||||||
|
label="Accent color"
|
||||||
|
control={ColorPicker}
|
||||||
|
value={customTheme.primaryColor || DefaultAppTheme.primaryColor}
|
||||||
|
onChange={val => update("primaryColor", val)}
|
||||||
|
props={{
|
||||||
|
spectrumTheme: $store.theme,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<PropertyControl
|
||||||
|
label="Hover"
|
||||||
|
control={ColorPicker}
|
||||||
|
value={customTheme.primaryColorHover || DefaultAppTheme.primaryColorHover}
|
||||||
|
onChange={val => update("primaryColorHover", val)}
|
||||||
|
props={{
|
||||||
|
spectrumTheme: $store.theme,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.info {
|
||||||
|
background-color: var(--background-alt);
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 4px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.info :global(svg) {
|
||||||
|
margin-right: 5px;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script>
|
||||||
|
import GeneralPanel from "./GeneralPanel.svelte"
|
||||||
|
import ThemePanel from "./ThemePanel.svelte"
|
||||||
|
import { selectedScreen } from "builderStore"
|
||||||
|
import Panel from "components/design/Panel.svelte"
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
import { ActionButton, Layout } from "@budibase/bbui"
|
||||||
|
|
||||||
|
let activeTab = "general"
|
||||||
|
const tabs = ["general", "theme"]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Panel
|
||||||
|
title={$selectedScreen.routing.route}
|
||||||
|
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
|
||||||
|
borderLeft
|
||||||
|
wide
|
||||||
|
>
|
||||||
|
<div slot="panel-header-content">
|
||||||
|
<div class="settings-tabs">
|
||||||
|
{#each tabs as tab}
|
||||||
|
<ActionButton
|
||||||
|
size="M"
|
||||||
|
quiet
|
||||||
|
selected={activeTab === tab}
|
||||||
|
on:click={() => {
|
||||||
|
activeTab = tab
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{capitalise(tab)}
|
||||||
|
</ActionButton>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Layout gap="S" paddingX="L" paddingY="XL">
|
||||||
|
{#if activeTab === "theme"}
|
||||||
|
<ThemePanel />
|
||||||
|
{:else}
|
||||||
|
<GeneralPanel />
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.settings-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
padding: 0 var(--spacing-l);
|
||||||
|
padding-bottom: var(--spacing-l);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script>
|
||||||
|
import { syncURLToState } from "helpers/urlStateSync"
|
||||||
|
import { store, selectedScreen } from "builderStore"
|
||||||
|
import * as routify from "@roxi/routify"
|
||||||
|
import { onDestroy } from "svelte"
|
||||||
|
import { findComponent } from "builderStore/componentUtils"
|
||||||
|
import ComponentSettingsPanel from "./_components/Component/ComponentSettingsPanel.svelte"
|
||||||
|
import NavigationPanel from "./_components/Navigation/index.svelte"
|
||||||
|
import ScreenSettingsPanel from "./_components/Screen/index.svelte"
|
||||||
|
|
||||||
|
$: componentId = $store.selectedComponentId
|
||||||
|
$: store.actions.websocket.selectResource(componentId)
|
||||||
|
$: params = routify.params
|
||||||
|
$: routeComponentId = $params.componentId
|
||||||
|
|
||||||
|
// Hide new component panel whenever component ID changes
|
||||||
|
const closeNewComponentPanel = url => {
|
||||||
|
if (url?.endsWith("/new")) {
|
||||||
|
url = url.replace("/new", "")
|
||||||
|
}
|
||||||
|
return { url }
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate = id => {
|
||||||
|
if (id === `${$store.selectedScreenId}-screen`) return true
|
||||||
|
if (id === `${$store.selectedScreenId}-navigation`) return true
|
||||||
|
|
||||||
|
return !!findComponent($selectedScreen.props, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep URL and state in sync for selected component ID
|
||||||
|
const stopSyncing = syncURLToState({
|
||||||
|
urlParam: "componentId",
|
||||||
|
stateKey: "selectedComponentId",
|
||||||
|
validate,
|
||||||
|
fallbackUrl: "../",
|
||||||
|
store,
|
||||||
|
routify,
|
||||||
|
beforeNavigate: closeNewComponentPanel,
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(stopSyncing)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if routeComponentId === `${$store.selectedScreenId}-screen`}
|
||||||
|
<ScreenSettingsPanel />
|
||||||
|
{:else if routeComponentId === `${$store.selectedScreenId}-navigation`}
|
||||||
|
<NavigationPanel />
|
||||||
|
{:else}
|
||||||
|
<ComponentSettingsPanel />
|
||||||
|
{/if}
|
||||||
|
<slot />
|
|
@ -0,0 +1 @@
|
||||||
|
<!-- Required to make Routify happy -->
|
|
@ -31,6 +31,10 @@
|
||||||
$: orderMap = createComponentOrderMap(componentList)
|
$: orderMap = createComponentOrderMap(componentList)
|
||||||
|
|
||||||
const getAllowedComponents = (allComponents, screen, component) => {
|
const getAllowedComponents = (allComponents, screen, component) => {
|
||||||
|
// Default to using the root screen container if no component specified
|
||||||
|
if (!component) {
|
||||||
|
component = screen.props
|
||||||
|
}
|
||||||
const path = findComponentPath(screen?.props, component?._id)
|
const path = findComponentPath(screen?.props, component?._id)
|
||||||
if (!path?.length) {
|
if (!path?.length) {
|
||||||
return []
|
return []
|
|
@ -1,32 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import DevicePreviewSelect from "./DevicePreviewSelect.svelte"
|
import DevicePreviewSelect from "./DevicePreviewSelect.svelte"
|
||||||
import AppPreview from "./AppPreview.svelte"
|
import AppPreview from "./AppPreview.svelte"
|
||||||
import { store, sortedScreens, screenHistoryStore } from "builderStore"
|
import { store, screenHistoryStore } from "builderStore"
|
||||||
import { Select } from "@budibase/bbui"
|
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
|
||||||
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
|
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
|
||||||
import { isActive } from "@roxi/routify"
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="app-panel">
|
<div class="app-panel">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<Select
|
<UndoRedoControl store={screenHistoryStore} />
|
||||||
placeholder={null}
|
|
||||||
options={$sortedScreens}
|
|
||||||
getOptionLabel={x => x.routing.route}
|
|
||||||
getOptionValue={x => x._id}
|
|
||||||
getOptionColour={x => RoleUtils.getRoleColour(x.routing.roleId)}
|
|
||||||
value={$store.selectedScreenId}
|
|
||||||
on:change={e => store.actions.screens.select(e.detail)}
|
|
||||||
quiet
|
|
||||||
autoWidth
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
{#if $isActive("./screens") || $isActive("./components")}
|
|
||||||
<UndoRedoControl store={screenHistoryStore} />
|
|
||||||
{/if}
|
|
||||||
{#if $store.clientFeatures.devicePreview}
|
{#if $store.clientFeatures.devicePreview}
|
||||||
<DevicePreviewSelect />
|
<DevicePreviewSelect />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -47,37 +31,24 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--spacing-m);
|
padding: 9px var(--spacing-m);
|
||||||
padding: var(--spacing-l) var(--spacing-xl);
|
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
margin-bottom: 9px;
|
||||||
justify-content: space-between;
|
}
|
||||||
align-items: flex-start;
|
|
||||||
gap: var(--spacing-l);
|
.header-left :global(div) {
|
||||||
margin: 0 2px;
|
border-right: none;
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
.header-left,
|
|
||||||
.header-right {
|
.header-right {
|
||||||
|
margin-left: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
.header-left {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
.header-left :global(> *) {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
.header-left :global(.spectrum-Picker) {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--spectrum-global-color-gray-900);
|
|
||||||
}
|
|
||||||
.content {
|
.content {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import {
|
import { store, selectedScreen, currentAsset } from "builderStore"
|
||||||
store,
|
|
||||||
selectedComponent,
|
|
||||||
selectedScreen,
|
|
||||||
selectedLayout,
|
|
||||||
currentAsset,
|
|
||||||
} from "builderStore"
|
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import {
|
import {
|
||||||
ProgressCircle,
|
ProgressCircle,
|
||||||
|
@ -20,12 +14,10 @@
|
||||||
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
|
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
|
||||||
import { findComponent, findComponentPath } from "builderStore/componentUtils"
|
import { findComponent, findComponentPath } from "builderStore/componentUtils"
|
||||||
import { isActive, goto } from "@roxi/routify"
|
import { isActive, goto } from "@roxi/routify"
|
||||||
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
|
|
||||||
|
|
||||||
let iframe
|
let iframe
|
||||||
let layout
|
let layout
|
||||||
let screen
|
let screen
|
||||||
let selectedComponentId
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
let idToDelete
|
let idToDelete
|
||||||
let loading = true
|
let loading = true
|
||||||
|
@ -39,36 +31,11 @@
|
||||||
BUDIBASE: "type",
|
BUDIBASE: "type",
|
||||||
}
|
}
|
||||||
|
|
||||||
const placeholderScreen = new Screen()
|
|
||||||
.name("Screen Placeholder")
|
|
||||||
.route("/")
|
|
||||||
.component("@budibase/standard-components/screenslot")
|
|
||||||
.instanceName("Content Placeholder")
|
|
||||||
.normalStyle({ flex: "1 1 auto" })
|
|
||||||
.json()
|
|
||||||
|
|
||||||
// Extract data to pass to the iframe
|
// Extract data to pass to the iframe
|
||||||
$: {
|
$: screen = $selectedScreen
|
||||||
// If viewing legacy layouts, always show the custom layout
|
|
||||||
if ($isActive("./layouts")) {
|
|
||||||
screen = placeholderScreen
|
|
||||||
layout = $selectedLayout
|
|
||||||
} else {
|
|
||||||
screen = $selectedScreen
|
|
||||||
layout = $store.layouts.find(layout => layout._id === screen?.layoutId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine selected component ID
|
// Determine selected component ID
|
||||||
$: {
|
$: selectedComponentId = $store.selectedComponentId
|
||||||
if ($isActive("./components")) {
|
|
||||||
selectedComponentId = $store.selectedComponentId
|
|
||||||
} else if ($isActive("./navigation")) {
|
|
||||||
selectedComponentId = "navigation"
|
|
||||||
} else {
|
|
||||||
selectedComponentId = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: previewData = {
|
$: previewData = {
|
||||||
appId: $store.appId,
|
appId: $store.appId,
|
||||||
|
@ -98,9 +65,7 @@
|
||||||
$: refreshContent(json)
|
$: refreshContent(json)
|
||||||
|
|
||||||
// Determine if the add component menu is active
|
// Determine if the add component menu is active
|
||||||
$: isAddingComponent = $isActive(
|
$: isAddingComponent = $isActive(`./${selectedComponentId}/new`)
|
||||||
`./components/${$selectedComponent?._id}/new`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Register handler to send custom to the preview
|
// Register handler to send custom to the preview
|
||||||
$: sendPreviewEvent = (name, payload) => {
|
$: sendPreviewEvent = (name, payload) => {
|
||||||
|
@ -152,9 +117,6 @@
|
||||||
error = event.error || "An unknown error occurred"
|
error = event.error || "An unknown error occurred"
|
||||||
} else if (type === "select-component" && data.id) {
|
} else if (type === "select-component" && data.id) {
|
||||||
$store.selectedComponentId = data.id
|
$store.selectedComponentId = data.id
|
||||||
if (!$isActive("./components")) {
|
|
||||||
$goto("./components")
|
|
||||||
}
|
|
||||||
} else if (type === "update-prop") {
|
} else if (type === "update-prop") {
|
||||||
await store.actions.components.updateSetting(data.prop, data.value)
|
await store.actions.components.updateSetting(data.prop, data.value)
|
||||||
} else if (type === "update-styles") {
|
} else if (type === "update-styles") {
|
||||||
|
@ -194,10 +156,6 @@
|
||||||
store.actions.components.copy(source, true, false)
|
store.actions.components.copy(source, true, false)
|
||||||
await store.actions.components.paste(destination, data.mode)
|
await store.actions.components.paste(destination, data.mode)
|
||||||
}
|
}
|
||||||
} else if (type === "click-nav") {
|
|
||||||
if (!$isActive("./navigation")) {
|
|
||||||
$goto("./navigation")
|
|
||||||
}
|
|
||||||
} else if (type === "request-add-component") {
|
} else if (type === "request-add-component") {
|
||||||
toggleAddComponent()
|
toggleAddComponent()
|
||||||
} else if (type === "highlight-setting") {
|
} else if (type === "highlight-setting") {
|
||||||
|
@ -247,11 +205,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleAddComponent = () => {
|
const toggleAddComponent = () => {
|
||||||
if (isAddingComponent) {
|
if ($isActive(`./:componentId/new`)) {
|
||||||
$goto(`../${$selectedScreen._id}/components/${$selectedComponent?._id}`)
|
$goto(`./:componentId`)
|
||||||
} else {
|
} else {
|
||||||
const id = $selectedComponent?._id || $selectedScreen?.props?._id
|
$goto(`./:componentId/new`)
|
||||||
$goto(`../${$selectedScreen._id}/components/${id}/new`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import { goto, isActive } from "@roxi/routify"
|
import { goto, isActive } from "@roxi/routify"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { isBuilderInputFocused } from "helpers"
|
|
||||||
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
let confirmEjectDialog
|
let confirmEjectDialog
|
||||||
|
@ -37,7 +36,7 @@
|
||||||
confirmEjectDialog.show()
|
confirmEjectDialog.show()
|
||||||
},
|
},
|
||||||
["Ctrl+Enter"]: () => {
|
["Ctrl+Enter"]: () => {
|
||||||
$goto("./new")
|
$goto(`./:componentId/new`)
|
||||||
},
|
},
|
||||||
["Delete"]: component => {
|
["Delete"]: component => {
|
||||||
// Don't show confirmation for the screen itself
|
// Don't show confirmation for the screen itself
|
||||||
|
@ -54,8 +53,8 @@
|
||||||
store.actions.components.selectNext()
|
store.actions.components.selectNext()
|
||||||
},
|
},
|
||||||
["Escape"]: () => {
|
["Escape"]: () => {
|
||||||
if ($isActive("./new")) {
|
if ($isActive(`./:componentId/new`)) {
|
||||||
$goto("./")
|
$goto(`./${$store.selectedComponentId}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -85,10 +84,13 @@
|
||||||
const handler = keyHandlers[key]
|
const handler = keyHandlers[key]
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
return false
|
return false
|
||||||
} else if (event) {
|
}
|
||||||
|
|
||||||
|
if (event && key !== "Escape") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
}
|
}
|
||||||
|
|
||||||
return await handler(component)
|
return await handler(component)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(error || "Error handling key press")
|
notifications.error(error || "Error handling key press")
|
||||||
|
@ -101,7 +103,13 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Ignore events when typing
|
// Ignore events when typing
|
||||||
if (isBuilderInputFocused(e)) {
|
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||||
|
const inCodeEditor =
|
||||||
|
document.activeElement?.classList?.contains("cm-content")
|
||||||
|
if (
|
||||||
|
(inCodeEditor || ["input", "textarea"].indexOf(activeTag) !== -1) &&
|
||||||
|
e.key !== "Escape"
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Key events are always for the selected component
|
// Key events are always for the selected component
|
|
@ -9,14 +9,14 @@
|
||||||
if (!bounds) {
|
if (!bounds) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const sidebarWidth = 259
|
const sidebarWidth = 310
|
||||||
const navItemHeight = 32
|
const navItemHeight = 32
|
||||||
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
|
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
|
||||||
let scrollBounds = scrollRef.getBoundingClientRect()
|
let scrollBounds = scrollRef.getBoundingClientRect()
|
||||||
let newOffsets = {}
|
let newOffsets = {}
|
||||||
|
|
||||||
// Calculate left offset
|
// Calculate left offset
|
||||||
const offsetX = bounds.left + bounds.width + scrollLeft - 36
|
const offsetX = bounds.left + bounds.width + scrollLeft + 16
|
||||||
if (offsetX > sidebarWidth) {
|
if (offsetX > sidebarWidth) {
|
||||||
newOffsets.left = offsetX - sidebarWidth
|
newOffsets.left = offsetX - sidebarWidth
|
||||||
} else {
|
} else {
|
||||||
|
@ -64,6 +64,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
on:scroll
|
||||||
bind:this={scrollRef}
|
bind:this={scrollRef}
|
||||||
on:drop={onDrop}
|
on:drop={onDrop}
|
||||||
ondragover="return false"
|
ondragover="return false"
|
||||||
|
@ -74,7 +75,6 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
padding: var(--spacing-xl) 0;
|
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
height: 0;
|
height: 0;
|
|
@ -107,6 +107,7 @@
|
||||||
id={`component-${component._id}`}
|
id={`component-${component._id}`}
|
||||||
>
|
>
|
||||||
<NavItem
|
<NavItem
|
||||||
|
compact
|
||||||
scrollable
|
scrollable
|
||||||
draggable
|
draggable
|
||||||
on:dragend={dndStore.actions.reset}
|
on:dragend={dndStore.actions.reset}
|
||||||
|
@ -117,7 +118,7 @@
|
||||||
text={getComponentText(component)}
|
text={getComponentText(component)}
|
||||||
icon={getComponentIcon(component)}
|
icon={getComponentIcon(component)}
|
||||||
withArrow={componentHasChildren(component)}
|
withArrow={componentHasChildren(component)}
|
||||||
indentLevel={level + 1}
|
indentLevel={level}
|
||||||
selected={$store.selectedComponentId === component._id}
|
selected={$store.selectedComponentId === component._id}
|
||||||
{opened}
|
{opened}
|
||||||
highlighted={isChildOfSelectedComponent(component)}
|
highlighted={isChildOfSelectedComponent(component)}
|
|
@ -0,0 +1,163 @@
|
||||||
|
<script>
|
||||||
|
import { notifications, Icon, Body } from "@budibase/bbui"
|
||||||
|
import { isActive, goto } from "@roxi/routify"
|
||||||
|
import { store, selectedScreen, userSelectedResourceMap } from "builderStore"
|
||||||
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
|
import ComponentTree from "./ComponentTree.svelte"
|
||||||
|
import { dndStore } from "./dndStore.js"
|
||||||
|
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
||||||
|
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||||
|
import { DropPosition } from "./dndStore"
|
||||||
|
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
|
||||||
|
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
|
||||||
|
|
||||||
|
let scrolling = false
|
||||||
|
|
||||||
|
const toNewComponentRoute = () => {
|
||||||
|
if ($isActive(`./:componentId/new`)) {
|
||||||
|
$goto(`./:componentId`)
|
||||||
|
} else {
|
||||||
|
$goto(`./:componentId/new`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDrop = async () => {
|
||||||
|
try {
|
||||||
|
await dndStore.actions.drop()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
notifications.error("Error saving component")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScroll = e => {
|
||||||
|
scrolling = e.target.scrollTop !== 0
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="components">
|
||||||
|
<div class="header" class:scrolling>
|
||||||
|
<Body size="S">Components</Body>
|
||||||
|
<div on:click={toNewComponentRoute} class="addButton">
|
||||||
|
<Icon name="Add" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-panel">
|
||||||
|
<ComponentScrollWrapper on:scroll={handleScroll}>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<NavItem
|
||||||
|
text="Screen"
|
||||||
|
indentLevel={0}
|
||||||
|
selected={$store.selectedComponentId ===
|
||||||
|
`${$store.selectedScreenId}-screen`}
|
||||||
|
opened
|
||||||
|
scrollable
|
||||||
|
icon="WebPage"
|
||||||
|
on:drop={onDrop}
|
||||||
|
on:click={() => {
|
||||||
|
$store.selectedComponentId = `${$store.selectedScreenId}-screen`
|
||||||
|
}}
|
||||||
|
id={`component-screen`}
|
||||||
|
selectedBy={$userSelectedResourceMap[
|
||||||
|
`${$store.selectedScreenId}-screen`
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
||||||
|
</NavItem>
|
||||||
|
<NavItem
|
||||||
|
text="Navigation"
|
||||||
|
indentLevel={0}
|
||||||
|
selected={$store.selectedComponentId ===
|
||||||
|
`${$store.selectedScreenId}-navigation`}
|
||||||
|
opened
|
||||||
|
scrollable
|
||||||
|
icon={$selectedScreen.showNavigation
|
||||||
|
? "Visibility"
|
||||||
|
: "VisibilityOff"}
|
||||||
|
on:drop={onDrop}
|
||||||
|
on:click={() => {
|
||||||
|
$store.selectedComponentId = `${$store.selectedScreenId}-navigation`
|
||||||
|
}}
|
||||||
|
id={`component-nav`}
|
||||||
|
selectedBy={$userSelectedResourceMap[
|
||||||
|
`${$store.selectedScreenId}-navigation`
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<ComponentTree
|
||||||
|
level={0}
|
||||||
|
components={$selectedScreen?.props._children}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Show drop indicators for the target and the parent -->
|
||||||
|
{#if $dndStore.dragging && $dndStore.valid}
|
||||||
|
<DNDPositionIndicator
|
||||||
|
component={$dndStore.target}
|
||||||
|
position={$dndStore.dropPosition}
|
||||||
|
/>
|
||||||
|
{#if $dndStore.dropPosition !== DropPosition.INSIDE}
|
||||||
|
<DNDPositionIndicator
|
||||||
|
component={$dndStore.targetParent}
|
||||||
|
position={DropPosition.INSIDE}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ComponentScrollWrapper>
|
||||||
|
</div>
|
||||||
|
<ComponentKeyHandler />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.components {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
height: 50px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: border-bottom 130ms ease-out;
|
||||||
|
}
|
||||||
|
.header.scrolling {
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.components :global(.nav-item) {
|
||||||
|
padding-right: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--grey-7);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
ul,
|
||||||
|
li {
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script>
|
||||||
|
import ScreenList from "./ScreenList/index.svelte"
|
||||||
|
import ComponentList from "./ComponentList/index.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<ScreenList />
|
||||||
|
<ComponentList />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.panel {
|
||||||
|
width: 310px;
|
||||||
|
height: 100%;
|
||||||
|
border-right: var(--border-light);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--background);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -56,7 +56,7 @@
|
||||||
const deleteScreen = async () => {
|
const deleteScreen = async () => {
|
||||||
try {
|
try {
|
||||||
await store.actions.screens.delete(screen)
|
await store.actions.screens.delete(screen)
|
||||||
notifications.success("Deleted screen successfully.")
|
notifications.success("Deleted screen successfully")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error("Error deleting screen")
|
notifications.error("Error deleting screen")
|
||||||
}
|
}
|
|
@ -26,7 +26,7 @@
|
||||||
<StatusLight square {color} />
|
<StatusLight square {color} />
|
||||||
{#if showTooltip}
|
{#if showTooltip}
|
||||||
<div class="tooltip">
|
<div class="tooltip">
|
||||||
<Tooltip textWrapping text={tooltip} direction="left" />
|
<Tooltip textWrapping text={tooltip} direction="right" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,13 +38,11 @@
|
||||||
.tooltip {
|
.tooltip {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
bottom: -5px;
|
||||||
left: calc(50% - 8px);
|
left: 13px;
|
||||||
transform: translateX(-100%) translateY(-50%);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 200px;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.tooltip :global(.spectrum-Tooltip) {
|
.tooltip :global(.spectrum-Tooltip) {
|
|
@ -0,0 +1,304 @@
|
||||||
|
<script>
|
||||||
|
import { Icon, Layout, Body } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
store,
|
||||||
|
sortedScreens,
|
||||||
|
userSelectedResourceMap,
|
||||||
|
screensHeight,
|
||||||
|
} from "builderStore"
|
||||||
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
|
import RoleIndicator from "./RoleIndicator.svelte"
|
||||||
|
import DropdownMenu from "./DropdownMenu.svelte"
|
||||||
|
import { onMount, tick } from "svelte"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
|
let search = false
|
||||||
|
let resizing = false
|
||||||
|
let searchValue = ""
|
||||||
|
let searchInput
|
||||||
|
let container
|
||||||
|
let screensContainer
|
||||||
|
let scrolling = false
|
||||||
|
let previousHeight = null
|
||||||
|
let dragOffset
|
||||||
|
|
||||||
|
$: filteredScreens = getFilteredScreens($sortedScreens, searchValue)
|
||||||
|
|
||||||
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
const openSearch = async () => {
|
||||||
|
search = true
|
||||||
|
await tick()
|
||||||
|
searchInput.focus()
|
||||||
|
screensContainer.scroll({ top: 0, behavior: "smooth" })
|
||||||
|
previousHeight = $screensHeight
|
||||||
|
$screensHeight = "calc(100% + 1px)"
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeSearch = async () => {
|
||||||
|
if (previousHeight) {
|
||||||
|
// Restore previous height and wait for animation
|
||||||
|
$screensHeight = previousHeight
|
||||||
|
previousHeight = null
|
||||||
|
await sleep(300)
|
||||||
|
}
|
||||||
|
search = false
|
||||||
|
searchValue = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFilteredScreens = (screens, search) => {
|
||||||
|
return screens.filter(screen => {
|
||||||
|
return !search || screen.routing.route.includes(search)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddButton = () => {
|
||||||
|
if (search) {
|
||||||
|
closeSearch()
|
||||||
|
} else {
|
||||||
|
$goto("../new")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = e => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
closeSearch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScroll = e => {
|
||||||
|
scrolling = e.target.scrollTop !== 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const startResizing = e => {
|
||||||
|
// Reset the height store to match the true height
|
||||||
|
$screensHeight = `${container.getBoundingClientRect().height}px`
|
||||||
|
|
||||||
|
// Store an offset to easily compute new height when moving the mouse
|
||||||
|
dragOffset = parseInt($screensHeight) - e.clientY
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
resizing = true
|
||||||
|
document.addEventListener("mousemove", resize)
|
||||||
|
document.addEventListener("mouseup", stopResizing)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resize = e => {
|
||||||
|
// Prevent negative heights as this screws with layout
|
||||||
|
const newHeight = Math.max(0, e.clientY + dragOffset)
|
||||||
|
if (newHeight == null || isNaN(newHeight)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$screensHeight = `${newHeight}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopResizing = () => {
|
||||||
|
resizing = false
|
||||||
|
document.removeEventListener("mousemove", resize)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Ensure we aren't stuck at 100% height from leaving while searching
|
||||||
|
if ($screensHeight == null || isNaN(parseInt($screensHeight))) {
|
||||||
|
$screensHeight = "210px"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={onKeyDown} />
|
||||||
|
<div
|
||||||
|
class="screens"
|
||||||
|
class:search
|
||||||
|
class:resizing
|
||||||
|
style={`height:${$screensHeight};`}
|
||||||
|
bind:this={container}
|
||||||
|
>
|
||||||
|
<div class="header" class:scrolling>
|
||||||
|
<input
|
||||||
|
readonly={!search}
|
||||||
|
bind:value={searchValue}
|
||||||
|
bind:this={searchInput}
|
||||||
|
class="input"
|
||||||
|
placeholder="Search for screens"
|
||||||
|
/>
|
||||||
|
<div class="title" class:hide={search}>
|
||||||
|
<Body size="S">Screens</Body>
|
||||||
|
</div>
|
||||||
|
<div on:click={openSearch} class="searchButton" class:hide={search}>
|
||||||
|
<Icon size="S" name="Search" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
on:click={handleAddButton}
|
||||||
|
class="addButton"
|
||||||
|
class:closeButton={search}
|
||||||
|
>
|
||||||
|
<Icon name="Add" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div on:scroll={handleScroll} bind:this={screensContainer} class="content">
|
||||||
|
{#if filteredScreens?.length}
|
||||||
|
{#each filteredScreens as screen (screen._id)}
|
||||||
|
<NavItem
|
||||||
|
icon={screen.routing.homeScreen ? "Home" : null}
|
||||||
|
indentLevel={0}
|
||||||
|
selected={$store.selectedScreenId === screen._id}
|
||||||
|
text={screen.routing.route}
|
||||||
|
on:click={() => store.actions.screens.select(screen._id)}
|
||||||
|
rightAlignIcon
|
||||||
|
showTooltip
|
||||||
|
selectedBy={$userSelectedResourceMap[screen._id]}
|
||||||
|
>
|
||||||
|
<DropdownMenu screenId={screen._id} />
|
||||||
|
<div slot="icon" class="icon">
|
||||||
|
<RoleIndicator roleId={screen.routing.roleId} />
|
||||||
|
</div>
|
||||||
|
</NavItem>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<Layout paddingY="none" paddingX="L">
|
||||||
|
<div class="no-results">
|
||||||
|
There aren't any screens matching that route
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="divider"
|
||||||
|
on:mousedown={startResizing}
|
||||||
|
on:dblclick={() => screensHeight.set("210px")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.screens {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 147px;
|
||||||
|
max-height: calc(100% - 147px);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.screens.search {
|
||||||
|
transition: height 300ms ease-out;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
.screens.resizing {
|
||||||
|
user-select: none;
|
||||||
|
cursor: row-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
height: 50px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 var(--spacing-l);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: border-bottom 130ms ease-out;
|
||||||
|
}
|
||||||
|
.header.scrolling {
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
position: absolute;
|
||||||
|
color: var(--ink);
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: var(--spectrum-alias-font-size-default);
|
||||||
|
width: 260px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.input::placeholder {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
.screens.search input {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1;
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
overflow: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.screens.resizing .content {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screens :global(.nav-item) {
|
||||||
|
padding-right: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchButton {
|
||||||
|
color: var(--grey-7);
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 10px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.searchButton:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
color: var(--grey-7);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 300ms ease-out;
|
||||||
|
}
|
||||||
|
.addButton:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
transform: translateY(50%);
|
||||||
|
height: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.divider:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
height: 2px;
|
||||||
|
width: 100%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
.divider:hover {
|
||||||
|
cursor: row-resize;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import { redirect } from "@roxi/routify"
|
|
||||||
|
|
||||||
$redirect("../screens")
|
|
||||||
</script>
|
|
|
@ -1,14 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { IconSideNav, IconSideNavItem } from "@budibase/bbui"
|
|
||||||
import * as routify from "@roxi/routify"
|
|
||||||
import AppPanel from "./_components/AppPanel.svelte"
|
import AppPanel from "./_components/AppPanel.svelte"
|
||||||
|
import * as routify from "@roxi/routify"
|
||||||
import { syncURLToState } from "helpers/urlStateSync"
|
import { syncURLToState } from "helpers/urlStateSync"
|
||||||
import { store, selectedScreen } from "builderStore"
|
import { store, selectedScreen } from "builderStore"
|
||||||
import { onDestroy } from "svelte"
|
import { onDestroy } from "svelte"
|
||||||
const { isActive, goto } = routify
|
import LeftPanel from "./_components/LeftPanel.svelte"
|
||||||
|
|
||||||
$: screenId = $store.selectedScreenId
|
|
||||||
$: store.actions.websocket.selectResource(screenId)
|
|
||||||
|
|
||||||
// Keep URL and state in sync for selected screen ID
|
// Keep URL and state in sync for selected screen ID
|
||||||
const stopSyncing = syncURLToState({
|
const stopSyncing = syncURLToState({
|
||||||
|
@ -23,51 +19,15 @@
|
||||||
onDestroy(stopSyncing)
|
onDestroy(stopSyncing)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="design">
|
{#if $selectedScreen}
|
||||||
<div class="icon-nav">
|
<div class="design">
|
||||||
<IconSideNav>
|
<div class="content">
|
||||||
<IconSideNavItem
|
<LeftPanel />
|
||||||
icon="WebPage"
|
|
||||||
tooltip="Screens"
|
|
||||||
active={$isActive("./screens")}
|
|
||||||
on:click={() => $goto("./screens")}
|
|
||||||
/>
|
|
||||||
<IconSideNavItem
|
|
||||||
icon="ViewList"
|
|
||||||
tooltip="Components"
|
|
||||||
active={$isActive("./components")}
|
|
||||||
on:click={() => $goto("./components")}
|
|
||||||
/>
|
|
||||||
<IconSideNavItem
|
|
||||||
icon="Brush"
|
|
||||||
tooltip="Theme"
|
|
||||||
active={$isActive("./theme")}
|
|
||||||
on:click={() => $goto("./theme")}
|
|
||||||
/>
|
|
||||||
<IconSideNavItem
|
|
||||||
icon="Link"
|
|
||||||
tooltip="Navigation"
|
|
||||||
active={$isActive("./navigation")}
|
|
||||||
on:click={() => $goto("./navigation")}
|
|
||||||
/>
|
|
||||||
{#if $store.layouts?.length}
|
|
||||||
<IconSideNavItem
|
|
||||||
icon="Experience"
|
|
||||||
tooltip="Layouts"
|
|
||||||
active={$isActive("./layouts")}
|
|
||||||
on:click={() => $goto("./layouts")}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</IconSideNav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
{#if $selectedScreen}
|
|
||||||
<slot />
|
|
||||||
<AppPanel />
|
<AppPanel />
|
||||||
{/if}
|
<slot />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.design {
|
.design {
|
||||||
|
@ -78,10 +38,7 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
.icon-nav {
|
|
||||||
background: var(--background);
|
|
||||||
border-right: var(--border-light);
|
|
||||||
}
|
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -89,17 +46,4 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
This is hacky, yes, but it's the only way to prevent routify from
|
|
||||||
remounting the iframe on route changes.
|
|
||||||
*/
|
|
||||||
.content :global(> *:last-child) {
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
.content :global(> *:first-child) {
|
|
||||||
order: 0;
|
|
||||||
}
|
|
||||||
.content :global(> *:nth-child(2)) {
|
|
||||||
order: 2;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
<script>
|
|
||||||
import Panel from "components/design/Panel.svelte"
|
|
||||||
import ComponentTree from "./ComponentTree.svelte"
|
|
||||||
import { dndStore } from "./dndStore.js"
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import { store, selectedScreen, userSelectedResourceMap } from "builderStore"
|
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
|
||||||
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
|
||||||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
|
||||||
import { DropPosition } from "./dndStore"
|
|
||||||
import { notifications, Button } from "@budibase/bbui"
|
|
||||||
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
|
|
||||||
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
|
|
||||||
|
|
||||||
const onDrop = async () => {
|
|
||||||
try {
|
|
||||||
await dndStore.actions.drop()
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
notifications.error("Error saving component")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Panel title="Components" showExpandIcon borderRight>
|
|
||||||
<div class="add-component">
|
|
||||||
<Button on:click={() => $goto("./new")} cta>Add component</Button>
|
|
||||||
</div>
|
|
||||||
<ComponentScrollWrapper>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<NavItem
|
|
||||||
text="Screen"
|
|
||||||
indentLevel={0}
|
|
||||||
selected={$store.selectedComponentId === $selectedScreen?.props._id}
|
|
||||||
opened
|
|
||||||
scrollable
|
|
||||||
icon="WebPage"
|
|
||||||
on:drop={onDrop}
|
|
||||||
on:click={() => {
|
|
||||||
$store.selectedComponentId = $selectedScreen?.props._id
|
|
||||||
}}
|
|
||||||
id={`component-${$selectedScreen?.props._id}`}
|
|
||||||
selectedBy={$userSelectedResourceMap[$selectedScreen?.props._id]}
|
|
||||||
>
|
|
||||||
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
|
||||||
</NavItem>
|
|
||||||
<ComponentTree
|
|
||||||
level={0}
|
|
||||||
components={$selectedScreen?.props._children}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Show drop indicators for the target and the parent -->
|
|
||||||
{#if $dndStore.dragging && $dndStore.valid}
|
|
||||||
<DNDPositionIndicator
|
|
||||||
component={$dndStore.target}
|
|
||||||
position={$dndStore.dropPosition}
|
|
||||||
/>
|
|
||||||
{#if $dndStore.dropPosition !== DropPosition.INSIDE}
|
|
||||||
<DNDPositionIndicator
|
|
||||||
component={$dndStore.targetParent}
|
|
||||||
position={DropPosition.INSIDE}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</ComponentScrollWrapper>
|
|
||||||
</Panel>
|
|
||||||
<ComponentKeyHandler />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.add-component {
|
|
||||||
padding: var(--spacing-xl) var(--spacing-l);
|
|
||||||
padding-bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding-left: 0;
|
|
||||||
margin: 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
ul,
|
|
||||||
li {
|
|
||||||
min-width: max-content;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,41 +0,0 @@
|
||||||
<script>
|
|
||||||
import { syncURLToState } from "helpers/urlStateSync"
|
|
||||||
import { store, selectedScreen } from "builderStore"
|
|
||||||
import * as routify from "@roxi/routify"
|
|
||||||
import { onDestroy } from "svelte"
|
|
||||||
import { findComponent } from "builderStore/componentUtils"
|
|
||||||
import ComponentListPanel from "./_components/navigation/ComponentListPanel.svelte"
|
|
||||||
import ComponentSettingsPanel from "./_components/settings/ComponentSettingsPanel.svelte"
|
|
||||||
|
|
||||||
$: componentId = $store.selectedComponentId
|
|
||||||
$: store.actions.websocket.selectResource(componentId)
|
|
||||||
|
|
||||||
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",
|
|
||||||
stateKey: "selectedComponentId",
|
|
||||||
validate: id => !!findComponent($selectedScreen.props, id),
|
|
||||||
fallbackUrl: "../",
|
|
||||||
store,
|
|
||||||
routify,
|
|
||||||
beforeNavigate: cleanUrl,
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(stopSyncing)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ComponentListPanel />
|
|
||||||
<ComponentSettingsPanel />
|
|
||||||
<slot />
|
|
|
@ -1,4 +0,0 @@
|
||||||
<!--
|
|
||||||
Placeholder file so that routify works.
|
|
||||||
No unique content is needed in this index page.
|
|
||||||
-->
|
|
|
@ -1,18 +0,0 @@
|
||||||
<script>
|
|
||||||
import { selectedScreen, selectedComponent } from "builderStore"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { redirect } from "@roxi/routify"
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if ($selectedComponent) {
|
|
||||||
// Navigate to the selected component if one exists
|
|
||||||
$redirect(`./${$selectedComponent._id}`)
|
|
||||||
} else if ($selectedScreen) {
|
|
||||||
// Otherwise the screen slot if a screen exists
|
|
||||||
$redirect(`./${$selectedScreen.props._id}`)
|
|
||||||
} else {
|
|
||||||
// Otherwise go up so we can select a new valid screen
|
|
||||||
$redirect("../")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
$redirect("./screens")
|
$redirect(`./${$store.selectedScreenId}-screen`)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
<script>
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
|
||||||
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let layout
|
|
||||||
|
|
||||||
let confirmDeleteDialog
|
|
||||||
|
|
||||||
const deleteLayout = async () => {
|
|
||||||
try {
|
|
||||||
await store.actions.layouts.delete(layout)
|
|
||||||
notifications.success("Layout deleted successfully")
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error(err?.message || "Error deleting layout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ActionMenu>
|
|
||||||
<div slot="control" class="icon">
|
|
||||||
<Icon size="S" hoverable name="MoreSmallList" />
|
|
||||||
</div>
|
|
||||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
|
||||||
</ActionMenu>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
bind:this={confirmDeleteDialog}
|
|
||||||
title="Confirm Deletion"
|
|
||||||
body={"Are you sure you wish to delete this layout?"}
|
|
||||||
okText="Delete layout"
|
|
||||||
onOk={deleteLayout}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.icon {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,29 +0,0 @@
|
||||||
<script>
|
|
||||||
import Panel from "components/design/Panel.svelte"
|
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import LayoutDropdownMenu from "./LayoutDropdownMenu.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Panel title="Layouts" borderRight>
|
|
||||||
<div class="layouts">
|
|
||||||
{#each $store.layouts as layout (layout._id)}
|
|
||||||
<NavItem
|
|
||||||
icon="Experience"
|
|
||||||
indentLevel={0}
|
|
||||||
selected={$store.selectedLayoutId === layout._id}
|
|
||||||
text={layout.name}
|
|
||||||
on:click={() => store.actions.layouts.select(layout._id)}
|
|
||||||
>
|
|
||||||
<LayoutDropdownMenu {layout} />
|
|
||||||
</NavItem>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.layouts {
|
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,53 +0,0 @@
|
||||||
<script>
|
|
||||||
import Panel from "components/design/Panel.svelte"
|
|
||||||
import { store, selectedLayout } from "builderStore"
|
|
||||||
import { Layout, Body, Button, Banner, notifications } from "@budibase/bbui"
|
|
||||||
import { Component } from "builderStore/store/screenTemplates/utils/Component"
|
|
||||||
|
|
||||||
const copyLayout = () => {
|
|
||||||
// Build an outer container component to put layout contents inside
|
|
||||||
let container = new Component("@budibase/standard-components/container")
|
|
||||||
.instanceName($selectedLayout.name)
|
|
||||||
.customProps({
|
|
||||||
gap: "M",
|
|
||||||
direction: "column",
|
|
||||||
hAlign: "stretch",
|
|
||||||
vAlign: "top",
|
|
||||||
size: "shrink",
|
|
||||||
})
|
|
||||||
.json()
|
|
||||||
|
|
||||||
// Attach layout components
|
|
||||||
container._children = $selectedLayout.props._children
|
|
||||||
|
|
||||||
// Replace the screenslot component with a container. This is better than
|
|
||||||
// simply removing it as it still shows its position.
|
|
||||||
container = JSON.parse(
|
|
||||||
JSON.stringify(container).replace(
|
|
||||||
"@budibase/standard-components/screenslot",
|
|
||||||
"@budibase/standard-components/container"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Copy new component structure
|
|
||||||
store.actions.components.copy(container)
|
|
||||||
notifications.success("Components copied successfully")
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Panel title={$selectedLayout?.name} icon="Experience" borderLeft wide>
|
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
|
||||||
<Banner type="warning" showCloseButton={false}>
|
|
||||||
Custom layouts are being deprecated. They will be removed in a future
|
|
||||||
release.
|
|
||||||
</Banner>
|
|
||||||
<Body size="S">
|
|
||||||
You can save the content of this layout by pressing the button below.
|
|
||||||
</Body>
|
|
||||||
<Body size="S">
|
|
||||||
This will copy all components inside your layout, which you can then paste
|
|
||||||
into a screen.
|
|
||||||
</Body>
|
|
||||||
<Button cta on:click={copyLayout}>Copy components</Button>
|
|
||||||
</Layout>
|
|
||||||
</Panel>
|
|
|
@ -1,20 +0,0 @@
|
||||||
<script>
|
|
||||||
import { syncURLToState } from "helpers/urlStateSync"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import * as routify from "@roxi/routify"
|
|
||||||
import { onDestroy } from "svelte"
|
|
||||||
|
|
||||||
// Keep URL and state in sync for selected component ID
|
|
||||||
const stopSyncing = syncURLToState({
|
|
||||||
urlParam: "layoutId",
|
|
||||||
stateKey: "selectedLayoutId",
|
|
||||||
validate: id => $store.layouts?.some(layout => layout._id === id),
|
|
||||||
fallbackUrl: "../",
|
|
||||||
store,
|
|
||||||
routify,
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(stopSyncing)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot />
|
|
|
@ -1,7 +0,0 @@
|
||||||
<script>
|
|
||||||
import LayoutListPanel from "./_components/LayoutListPanel.svelte"
|
|
||||||
import LayoutSettingsPanel from "./_components/LayoutSettingsPanel.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<LayoutListPanel />
|
|
||||||
<LayoutSettingsPanel />
|
|
|
@ -1,12 +0,0 @@
|
||||||
<script>
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import { redirect } from "@roxi/routify"
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (!$store.layouts?.length) {
|
|
||||||
$redirect("../")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot />
|
|
|
@ -1,12 +0,0 @@
|
||||||
<script>
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { redirect } from "@roxi/routify"
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if ($store.layouts?.length) {
|
|
||||||
$redirect(`./${$store.layouts[0]._id}`)
|
|
||||||
}
|
|
||||||
// The redirection when no layouts exist is handled by the routify layout
|
|
||||||
})
|
|
||||||
</script>
|
|
|
@ -1,33 +0,0 @@
|
||||||
<script>
|
|
||||||
import Panel from "components/design/Panel.svelte"
|
|
||||||
import { Body, Layout, Banner } from "@budibase/bbui"
|
|
||||||
import { selectedScreen, store } from "builderStore"
|
|
||||||
import { get } from "svelte/store"
|
|
||||||
|
|
||||||
const removeCustomLayout = async () => {
|
|
||||||
return store.actions.screens.removeCustomLayout(get(selectedScreen))
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Panel borderLeft title="Navigation" icon="InfoOutline" wide>
|
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
|
||||||
{#if $selectedScreen.layoutId}
|
|
||||||
<Banner
|
|
||||||
type="warning"
|
|
||||||
extraButtonText="Detach custom layout"
|
|
||||||
extraButtonAction={removeCustomLayout}
|
|
||||||
showCloseButton={false}
|
|
||||||
>
|
|
||||||
You can't preview your navigation settings using this screen as it uses
|
|
||||||
a custom layout, which is deprecated
|
|
||||||
</Banner>
|
|
||||||
{/if}
|
|
||||||
<Body size="S">
|
|
||||||
Your navigation is configured for all the screens within your app.
|
|
||||||
</Body>
|
|
||||||
<Body size="S">
|
|
||||||
You can hide and show your navigation for each screen in the screen
|
|
||||||
settings.
|
|
||||||
</Body>
|
|
||||||
</Layout>
|
|
||||||
</Panel>
|
|
|
@ -1,110 +0,0 @@
|
||||||
<script>
|
|
||||||
import Panel from "components/design/Panel.svelte"
|
|
||||||
import {
|
|
||||||
Layout,
|
|
||||||
Label,
|
|
||||||
ActionGroup,
|
|
||||||
ActionButton,
|
|
||||||
Checkbox,
|
|
||||||
Select,
|
|
||||||
ColorPicker,
|
|
||||||
Input,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import NavigationLinksEditor from "./NavigationLinksEditor.svelte"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import { DefaultAppTheme } from "constants"
|
|
||||||
|
|
||||||
const update = async (key, value) => {
|
|
||||||
try {
|
|
||||||
let navigation = $store.navigation
|
|
||||||
navigation[key] = value
|
|
||||||
await store.actions.navigation.save(navigation)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error updating navigation settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Panel title="Navigation" borderRight>
|
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
|
||||||
<NavigationLinksEditor />
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Label>Position</Label>
|
|
||||||
<ActionGroup quiet>
|
|
||||||
<ActionButton
|
|
||||||
selected={$store.navigation.navigation === "Top"}
|
|
||||||
quiet={$store.navigation.navigation !== "Top"}
|
|
||||||
icon="PaddingTop"
|
|
||||||
on:click={() => update("navigation", "Top")}
|
|
||||||
/>
|
|
||||||
<ActionButton
|
|
||||||
selected={$store.navigation.navigation === "Left"}
|
|
||||||
quiet={$store.navigation.navigation !== "Left"}
|
|
||||||
icon="PaddingLeft"
|
|
||||||
on:click={() => update("navigation", "Left")}
|
|
||||||
/>
|
|
||||||
</ActionGroup>
|
|
||||||
</Layout>
|
|
||||||
{#if $store.navigation.navigation === "Top"}
|
|
||||||
<Checkbox
|
|
||||||
text="Sticky header"
|
|
||||||
value={$store.navigation.sticky}
|
|
||||||
on:change={e => update("sticky", e.detail)}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label="Width"
|
|
||||||
options={["Max", "Large", "Medium", "Small"]}
|
|
||||||
plaveholder={null}
|
|
||||||
value={$store.navigation.navWidth}
|
|
||||||
on:change={e => update("navWidth", e.detail)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Checkbox
|
|
||||||
text="Logo"
|
|
||||||
value={!$store.navigation.hideLogo}
|
|
||||||
on:change={e => update("hideLogo", !e.detail)}
|
|
||||||
/>
|
|
||||||
{#if !$store.navigation.hideLogo}
|
|
||||||
<Input
|
|
||||||
value={$store.navigation.logoUrl}
|
|
||||||
on:change={e => update("logoUrl", e.detail)}
|
|
||||||
placeholder="Add logo URL"
|
|
||||||
updateOnChange={false}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Checkbox
|
|
||||||
text="Title"
|
|
||||||
value={!$store.navigation.hideTitle}
|
|
||||||
on:change={e => update("hideTitle", !e.detail)}
|
|
||||||
/>
|
|
||||||
{#if !$store.navigation.hideTitle}
|
|
||||||
<Input
|
|
||||||
value={$store.navigation.title}
|
|
||||||
on:change={e => update("title", e.detail)}
|
|
||||||
placeholder="Add title"
|
|
||||||
updateOnChange={false}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Label>Background color</Label>
|
|
||||||
<ColorPicker
|
|
||||||
spectrumTheme={$store.theme}
|
|
||||||
value={$store.navigation.navBackground || DefaultAppTheme.navBackground}
|
|
||||||
on:change={e => update("navBackground", e.detail)}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Label>Text color</Label>
|
|
||||||
<ColorPicker
|
|
||||||
spectrumTheme={$store.theme}
|
|
||||||
value={$store.navigation.navTextColor || DefaultAppTheme.navTextColor}
|
|
||||||
on:change={e => update("navTextColor", e.detail)}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
|
||||||
</Panel>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<script>
|
|
||||||
import NavigationSettingsPanel from "./_components/NavigationSettingsPanel.svelte"
|
|
||||||
import NavigationInfoPanel from "./_components/NavigationInfoPanel.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NavigationSettingsPanel />
|
|
||||||
<NavigationInfoPanel />
|
|
|
@ -1,75 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Search, Layout, Select, Body, Button } from "@budibase/bbui"
|
|
||||||
import Panel from "components/design/Panel.svelte"
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import { roles } from "stores/backend"
|
|
||||||
import { store, sortedScreens, userSelectedResourceMap } from "builderStore"
|
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
|
||||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
|
||||||
import RoleIndicator from "./RoleIndicator.svelte"
|
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
|
||||||
|
|
||||||
let searchString
|
|
||||||
let accessRole = "all"
|
|
||||||
|
|
||||||
$: filteredScreens = getFilteredScreens(
|
|
||||||
$sortedScreens,
|
|
||||||
searchString,
|
|
||||||
accessRole
|
|
||||||
)
|
|
||||||
|
|
||||||
const getFilteredScreens = (screens, search, role) => {
|
|
||||||
return screens.filter(screen => {
|
|
||||||
const searchMatch = !search || screen.routing.route.includes(search)
|
|
||||||
const roleMatch =
|
|
||||||
!role || role === "all" || screen.routing.roleId === role
|
|
||||||
return searchMatch && roleMatch
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Panel title="Screens" borderRight>
|
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
|
||||||
<Button on:click={() => $goto("../../new")} cta>Add screen</Button>
|
|
||||||
<Search
|
|
||||||
placeholder="Search"
|
|
||||||
value={searchString}
|
|
||||||
on:change={e => (searchString = e.detail)}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
bind:value={accessRole}
|
|
||||||
placeholder={null}
|
|
||||||
getOptionLabel={role => role.name}
|
|
||||||
getOptionValue={role => role._id}
|
|
||||||
getOptionColour={role => {
|
|
||||||
if (role?._id === "all") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return RoleUtils.getRoleColour(role._id)
|
|
||||||
}}
|
|
||||||
options={[{ name: "All screens", _id: "all" }, ...$roles]}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
{#each filteredScreens as screen (screen._id)}
|
|
||||||
<NavItem
|
|
||||||
icon={screen.routing.homeScreen ? "Home" : null}
|
|
||||||
indentLevel={0}
|
|
||||||
selected={$store.selectedScreenId === screen._id}
|
|
||||||
text={screen.routing.route}
|
|
||||||
on:click={() => store.actions.screens.select(screen._id)}
|
|
||||||
rightAlignIcon
|
|
||||||
showTooltip
|
|
||||||
selectedBy={$userSelectedResourceMap[screen._id]}
|
|
||||||
>
|
|
||||||
<ScreenDropdownMenu screenId={screen._id} />
|
|
||||||
<RoleIndicator slot="right" roleId={screen.routing.roleId} />
|
|
||||||
</NavItem>
|
|
||||||
{/each}
|
|
||||||
{#if !filteredScreens?.length}
|
|
||||||
<Layout paddingY="" paddingX="L">
|
|
||||||
<Body size="S">
|
|
||||||
There aren't any screens matching the current filters
|
|
||||||
</Body>
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
</Panel>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<script>
|
|
||||||
import { selectedScreen } from "builderStore"
|
|
||||||
import ScreenListPanel from "./_components/ScreenListPanel.svelte"
|
|
||||||
import ScreenSettingsPanel from "./_components/ScreenSettingsPanel.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ScreenListPanel />
|
|
||||||
{#if $selectedScreen}
|
|
||||||
{#key $selectedScreen._id}
|
|
||||||
<ScreenSettingsPanel />
|
|
||||||
{/key}
|
|
||||||
{/if}
|
|
|
@ -1,12 +0,0 @@
|
||||||
<script>
|
|
||||||
import Panel from "components/design/Panel.svelte"
|
|
||||||
import { Body, Layout } from "@budibase/bbui"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Panel borderLeft title="Theme" icon="InfoOutline" wide>
|
|
||||||
<Layout paddingX="L" paddingY="XL">
|
|
||||||
<Body size="S">
|
|
||||||
Your theme is set across all the screens within your app.
|
|
||||||
</Body>
|
|
||||||
</Layout>
|
|
||||||
</Panel>
|
|
|
@ -1,55 +0,0 @@
|
||||||
<script>
|
|
||||||
import Panel from "components/design/Panel.svelte"
|
|
||||||
import { Layout, Label, ColorPicker, notifications } from "@budibase/bbui"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import { get } from "svelte/store"
|
|
||||||
import { DefaultAppTheme } from "constants"
|
|
||||||
import AppThemeSelect from "./AppThemeSelect.svelte"
|
|
||||||
import ButtonRoundnessSelect from "./ButtonRoundnessSelect.svelte"
|
|
||||||
|
|
||||||
$: customTheme = $store.customTheme || {}
|
|
||||||
|
|
||||||
const update = async (property, value) => {
|
|
||||||
try {
|
|
||||||
store.actions.customTheme.save({
|
|
||||||
...get(store).customTheme,
|
|
||||||
[property]: value,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error updating custom theme")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Panel title="Theme" borderRight>
|
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Label>Theme</Label>
|
|
||||||
<AppThemeSelect />
|
|
||||||
</Layout>
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Label>Button roundness</Label>
|
|
||||||
<ButtonRoundnessSelect
|
|
||||||
{customTheme}
|
|
||||||
on:change={e => update("buttonBorderRadius", e.detail)}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Label>Accent color</Label>
|
|
||||||
<ColorPicker
|
|
||||||
spectrumTheme={$store.theme}
|
|
||||||
value={customTheme.primaryColor || DefaultAppTheme.primaryColor}
|
|
||||||
on:change={e => update("primaryColor", e.detail)}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Label>Accent color (hover)</Label>
|
|
||||||
<ColorPicker
|
|
||||||
spectrumTheme={$store.theme}
|
|
||||||
value={customTheme.primaryColorHover ||
|
|
||||||
DefaultAppTheme.primaryColorHover}
|
|
||||||
on:change={e => update("primaryColorHover", e.detail)}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
|
||||||
</Panel>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<script>
|
|
||||||
import ThemeSettingsPanel from "./_components/ThemeSettingsPanel.svelte"
|
|
||||||
import ThemeInfoPanel from "./_components/ThemeInfoPanel.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ThemeSettingsPanel />
|
|
||||||
<ThemeInfoPanel />
|
|
|
@ -58,16 +58,17 @@
|
||||||
const response = await store.actions.screens.save(screen)
|
const response = await store.actions.screens.save(screen)
|
||||||
screenId = response._id
|
screenId = response._id
|
||||||
|
|
||||||
// Add link in layout for list screens
|
// Add link in layout. We only ever actually create 1 screen now, even
|
||||||
if (screen.props._instanceName.endsWith("List")) {
|
// for autoscreens, so it's always safe to do this.
|
||||||
await store.actions.links.save(
|
await store.actions.links.save(
|
||||||
screen.routing.route,
|
screen.routing.route,
|
||||||
capitalise(screen.routing.route.split("/")[1])
|
capitalise(screen.routing.route.split("/")[1])
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Go to new screen
|
||||||
$goto(`./${screenId}`)
|
$goto(`./${screenId}`)
|
||||||
|
store.actions.screens.select(screenId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
notifications.error("Error creating screens")
|
notifications.error("Error creating screens")
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
@ -0,0 +1,105 @@
|
||||||
|
<script>
|
||||||
|
import { Body } from "@budibase/bbui"
|
||||||
|
import CreationPage from "components/common/CreationPage.svelte"
|
||||||
|
import blankImage from "./blank.png"
|
||||||
|
import tableImage from "./table.png"
|
||||||
|
import CreateScreenModal from "./CreateScreenModal.svelte"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
|
export let onClose = null
|
||||||
|
|
||||||
|
let createScreenModal
|
||||||
|
|
||||||
|
$: hasScreens = $store.screens?.length
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<CreationPage
|
||||||
|
showClose={!!onClose}
|
||||||
|
{onClose}
|
||||||
|
heading={hasScreens ? "Create new screen" : "Create your first screen"}
|
||||||
|
>
|
||||||
|
<div class="subHeading">
|
||||||
|
<Body>Start from scratch or create screens from your data</Body>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<div class="card" on:click={() => createScreenModal.show("blank")}>
|
||||||
|
<div class="image">
|
||||||
|
<img alt="" src={blankImage} />
|
||||||
|
</div>
|
||||||
|
<div class="text">
|
||||||
|
<Body size="S">Blank screen</Body>
|
||||||
|
<Body size="XS">Add an empty blank screen</Body>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" on:click={() => createScreenModal.show("table")}>
|
||||||
|
<div class="image">
|
||||||
|
<img alt="" src={tableImage} />
|
||||||
|
</div>
|
||||||
|
<div class="text">
|
||||||
|
<Body size="S">Table</Body>
|
||||||
|
<Body size="XS">View, edit and delete rows on a table</Body>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CreationPage>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CreateScreenModal bind:this={createScreenModal} />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
padding: 28px 40px 40px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subHeading :global(p) {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
max-width: 235px;
|
||||||
|
transition: filter 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 127px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
border: 1px solid var(--grey-4);
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
padding: 8px 16px 13px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text :global(p:nth-child(1)) {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text :global(p:nth-child(2)) {
|
||||||
|
color: var(--grey-6);
|
||||||
|
}
|
||||||
|
</style>
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
@ -1,104 +1,23 @@
|
||||||
<script>
|
<script>
|
||||||
import { Body } from "@budibase/bbui"
|
import NewScreen from "./_components/NewScreen/index.svelte"
|
||||||
import CreationPage from "components/common/CreationPage.svelte"
|
|
||||||
import blankImage from "./blank.png"
|
|
||||||
import tableImage from "./table.png"
|
|
||||||
import CreateScreenModal from "./_components/CreateScreenModal.svelte"
|
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
let createScreenModal
|
$: onClose = getOnClose($store)
|
||||||
|
|
||||||
$: hasScreens = $store.screens?.length
|
const getOnClose = ({ screens, selectedScreenId }) => {
|
||||||
|
if (!screens?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (selectedScreenId) {
|
||||||
|
return () => {
|
||||||
|
$goto(`./${selectedScreenId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
$goto(`./${screens[0]._id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="page">
|
<NewScreen {onClose} />
|
||||||
<CreationPage
|
|
||||||
showClose={$store.screens.length > 0}
|
|
||||||
onClose={() => $goto(`./${$store.screens[0]._id}`)}
|
|
||||||
heading={hasScreens ? "Create new screen" : "Create your first screen"}
|
|
||||||
>
|
|
||||||
<div class="subHeading">
|
|
||||||
<Body>Start from scratch or create screens from your data</Body>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cards">
|
|
||||||
<div class="card" on:click={() => createScreenModal.show("blank")}>
|
|
||||||
<div class="image">
|
|
||||||
<img alt="" src={blankImage} />
|
|
||||||
</div>
|
|
||||||
<div class="text">
|
|
||||||
<Body size="S">Blank screen</Body>
|
|
||||||
<Body size="XS">Add an empty blank screen</Body>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" on:click={() => createScreenModal.show("table")}>
|
|
||||||
<div class="image">
|
|
||||||
<img alt="" src={tableImage} />
|
|
||||||
</div>
|
|
||||||
<div class="text">
|
|
||||||
<Body size="S">Table</Body>
|
|
||||||
<Body size="XS">View, edit and delete rows on a table</Body>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CreationPage>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CreateScreenModal bind:this={createScreenModal} />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.page {
|
|
||||||
padding: 28px 40px 40px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subHeading :global(p) {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 12px;
|
|
||||||
margin-bottom: 36px;
|
|
||||||
color: var(--spectrum-global-color-gray-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cards {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
max-width: 235px;
|
|
||||||
transition: filter 150ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
filter: brightness(1.1);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
border-radius: 4px 4px 0 0;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 127px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
border: 1px solid var(--grey-4);
|
|
||||||
border-radius: 0 0 4px 4px;
|
|
||||||
padding: 8px 16px 13px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text :global(p:nth-child(1)) {
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text :global(p:nth-child(2)) {
|
|
||||||
color: var(--grey-6);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
|
|
||||||
export let instance = {}
|
export let instance = {}
|
||||||
export let isLayout = false
|
export let isLayout = false
|
||||||
export let isScreen = false
|
export let isRoot = false
|
||||||
export let isBlock = false
|
export let isBlock = false
|
||||||
|
|
||||||
// Get parent contexts
|
// Get parent contexts
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
// Extract component instance info
|
// Extract component instance info
|
||||||
$: children = instance._children || []
|
$: children = instance._children || []
|
||||||
$: id = instance._id
|
$: id = instance._id
|
||||||
$: name = isScreen ? "Screen" : instance._instanceName
|
$: name = isRoot ? "Screen" : instance._instanceName
|
||||||
$: icon = definition?.icon
|
$: icon = definition?.icon
|
||||||
|
|
||||||
// Determine if the component is selected or is part of the critical path
|
// Determine if the component is selected or is part of the critical path
|
||||||
|
@ -133,13 +133,13 @@
|
||||||
$: builderInteractive =
|
$: builderInteractive =
|
||||||
$builderStore.inBuilder && insideScreenslot && !isBlock && !instance.static
|
$builderStore.inBuilder && insideScreenslot && !isBlock && !instance.static
|
||||||
$: devToolsInteractive = $devToolsStore.allowSelection && !isBlock
|
$: devToolsInteractive = $devToolsStore.allowSelection && !isBlock
|
||||||
$: interactive = builderInteractive || devToolsInteractive
|
$: interactive = !isRoot && (builderInteractive || devToolsInteractive)
|
||||||
$: editing = editable && selected && $builderStore.editMode
|
$: editing = editable && selected && $builderStore.editMode
|
||||||
$: draggable =
|
$: draggable =
|
||||||
!inDragPath &&
|
!inDragPath &&
|
||||||
interactive &&
|
interactive &&
|
||||||
!isLayout &&
|
!isLayout &&
|
||||||
!isScreen &&
|
!isRoot &&
|
||||||
definition?.draggable !== false
|
definition?.draggable !== false
|
||||||
$: droppable = interactive
|
$: droppable = interactive
|
||||||
$: builderHidden =
|
$: builderHidden =
|
||||||
|
@ -475,7 +475,7 @@
|
||||||
node.style.scrollMargin = "100px"
|
node.style.scrollMargin = "100px"
|
||||||
node.scrollIntoView({
|
node.scrollIntoView({
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
block: "start",
|
block: "nearest",
|
||||||
inline: "start",
|
inline: "start",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -538,7 +538,7 @@
|
||||||
<svelte:self instance={child} />
|
<svelte:self instance={child} />
|
||||||
{/each}
|
{/each}
|
||||||
{:else if emptyState}
|
{:else if emptyState}
|
||||||
{#if isScreen}
|
{#if isRoot}
|
||||||
<ScreenPlaceholder />
|
<ScreenPlaceholder />
|
||||||
{:else}
|
{:else}
|
||||||
<EmptyPlaceholder />
|
<EmptyPlaceholder />
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
{#if $routeStore.routerLoaded}
|
{#if $routeStore.routerLoaded}
|
||||||
{#key screenDefinition?._id}
|
{#key screenDefinition?._id}
|
||||||
<Provider key="url" data={params}>
|
<Provider key="url" data={params}>
|
||||||
<Component isScreen instance={screenDefinition} />
|
<Component isRoot instance={screenDefinition} />
|
||||||
</Provider>
|
</Provider>
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -72,19 +72,27 @@
|
||||||
$context.device.height
|
$context.device.height
|
||||||
)
|
)
|
||||||
$: autoCloseSidePanel = !$builderStore.inBuilder && $sidePanelStore.open
|
$: autoCloseSidePanel = !$builderStore.inBuilder && $sidePanelStore.open
|
||||||
|
$: screenId = $builderStore.inBuilder
|
||||||
|
? `${$builderStore.screen?._id}-screen`
|
||||||
|
: "screen"
|
||||||
|
$: navigationId = $builderStore.inBuilder
|
||||||
|
? `${$builderStore.screen?._id}-navigation`
|
||||||
|
: "navigation"
|
||||||
|
|
||||||
// Scroll navigation into view if selected
|
// Scroll navigation into view if selected.
|
||||||
|
// Memoize into a primitive to avoid spamming this whenever builder store
|
||||||
|
// changes.
|
||||||
|
$: selected =
|
||||||
|
$builderStore.inBuilder &&
|
||||||
|
$builderStore.selectedComponentId?.endsWith("-navigation")
|
||||||
$: {
|
$: {
|
||||||
if (
|
if (selected) {
|
||||||
$builderStore.inBuilder &&
|
|
||||||
$builderStore.selectedComponentId === "navigation"
|
|
||||||
) {
|
|
||||||
const node = document.getElementsByClassName("nav-wrapper")?.[0]
|
const node = document.getElementsByClassName("nav-wrapper")?.[0]
|
||||||
if (node) {
|
if (node) {
|
||||||
node.style.scrollMargin = "100px"
|
node.style.scrollMargin = "100px"
|
||||||
node.scrollIntoView({
|
node.scrollIntoView({
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
block: "start",
|
block: "nearest",
|
||||||
inline: "start",
|
inline: "start",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -146,26 +154,29 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="layout layout--{typeClass}"
|
class="component {screenId} layout layout--{typeClass}"
|
||||||
use:styleable={$component.styles}
|
use:styleable={$component.styles}
|
||||||
class:desktop={!mobile}
|
class:desktop={!mobile}
|
||||||
class:mobile={!!mobile}
|
class:mobile={!!mobile}
|
||||||
|
data-id={screenId}
|
||||||
|
data-name="Screen"
|
||||||
|
data-icon="WebPage"
|
||||||
>
|
>
|
||||||
<div class="layout-body">
|
<div class="{screenId}-dom screen-wrapper layout-body">
|
||||||
{#if typeClass !== "none"}
|
{#if typeClass !== "none"}
|
||||||
<div
|
<div
|
||||||
class="interactive component navigation"
|
class="interactive component {navigationId}"
|
||||||
data-id="navigation"
|
data-id={navigationId}
|
||||||
data-name="Navigation"
|
data-name="Navigation"
|
||||||
data-icon="Link"
|
data-icon="Visibility"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="nav-wrapper"
|
class="nav-wrapper {navigationId}-dom"
|
||||||
class:sticky
|
class:sticky
|
||||||
class:hidden={$routeStore.queryParams?.peek}
|
class:hidden={$routeStore.queryParams?.peek}
|
||||||
class:clickable={$builderStore.inBuilder}
|
class:clickable={$builderStore.inBuilder}
|
||||||
on:click={$builderStore.inBuilder
|
on:click={$builderStore.inBuilder
|
||||||
? builderStore.actions.clickNav
|
? builderStore.actions.selectComponent(navigationId)
|
||||||
: null}
|
: null}
|
||||||
style={navStyle}
|
style={navStyle}
|
||||||
>
|
>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$: settings = getBarSettings(definition)
|
$: settings = getBarSettings(definition)
|
||||||
$: isScreen = id === $builderStore.screen?.props?._id
|
$: isRoot = id === $builderStore.screen?.props?._id
|
||||||
|
|
||||||
const getBarSettings = definition => {
|
const getBarSettings = definition => {
|
||||||
let allSettings = []
|
let allSettings = []
|
||||||
|
@ -160,11 +160,11 @@
|
||||||
{:else if setting.type === "color"}
|
{:else if setting.type === "color"}
|
||||||
<SettingsColorPicker prop={setting.key} />
|
<SettingsColorPicker prop={setting.key} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if setting.barSeparator !== false && (settings.length != idx + 1 || !isScreen)}
|
{#if setting.barSeparator !== false && (settings.length != idx + 1 || !isRoot)}
|
||||||
<div class="divider" />
|
<div class="divider" />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{#if !isScreen}
|
{#if !isRoot}
|
||||||
<SettingsButton
|
<SettingsButton
|
||||||
icon="Duplicate"
|
icon="Duplicate"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
|
@ -80,9 +80,6 @@ const createBuilderStore = () => {
|
||||||
}
|
}
|
||||||
store.update(state => ({ ...state, editMode: enabled }))
|
store.update(state => ({ ...state, editMode: enabled }))
|
||||||
},
|
},
|
||||||
clickNav: () => {
|
|
||||||
eventStore.actions.dispatchEvent("click-nav")
|
|
||||||
},
|
|
||||||
requestAddComponent: () => {
|
requestAddComponent: () => {
|
||||||
eventStore.actions.dispatchEvent("request-add-component")
|
eventStore.actions.dispatchEvent("request-add-component")
|
||||||
},
|
},
|
||||||
|
|
|
@ -281,12 +281,7 @@ async function performAppCreate(ctx: UserCtx) {
|
||||||
title: name,
|
title: name,
|
||||||
navWidth: "Large",
|
navWidth: "Large",
|
||||||
navBackground: "var(--spectrum-global-color-gray-100)",
|
navBackground: "var(--spectrum-global-color-gray-100)",
|
||||||
links: [
|
links: [],
|
||||||
{
|
|
||||||
url: "/home",
|
|
||||||
text: "Home",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
theme: "spectrum--light",
|
theme: "spectrum--light",
|
||||||
customTheme: {
|
customTheme: {
|
||||||
|
|
Loading…
Reference in New Issue